1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 package com.android.systemui.qs.external;
17 
18 import android.content.BroadcastReceiver;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.graphics.drawable.Icon;
26 import android.os.Binder;
27 import android.os.Handler;
28 import android.os.IBinder;
29 import android.os.Looper;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.service.quicksettings.IQSService;
33 import android.service.quicksettings.Tile;
34 import android.service.quicksettings.TileService;
35 import android.util.ArrayMap;
36 import android.util.Log;
37 
38 import com.android.internal.statusbar.StatusBarIcon;
39 import com.android.systemui.Dependency;
40 import com.android.systemui.qs.QSTileHost;
41 import com.android.systemui.statusbar.phone.StatusBarIconController;
42 import com.android.systemui.statusbar.policy.KeyguardMonitor;
43 
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.Comparator;
47 
48 /**
49  * Runs the day-to-day operations of which tiles should be bound and when.
50  */
51 public class TileServices extends IQSService.Stub {
52     static final int DEFAULT_MAX_BOUND = 3;
53     static final int REDUCED_MAX_BOUND = 1;
54     private static final String TAG = "TileServices";
55 
56     private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>();
57     private final ArrayMap<ComponentName, CustomTile> mTiles = new ArrayMap<>();
58     private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>();
59     private final Context mContext;
60     private final Handler mHandler;
61     private final Handler mMainHandler;
62     private final QSTileHost mHost;
63 
64     private int mMaxBound = DEFAULT_MAX_BOUND;
65 
TileServices(QSTileHost host, Looper looper)66     public TileServices(QSTileHost host, Looper looper) {
67         mHost = host;
68         mContext = mHost.getContext();
69         mContext.registerReceiver(mRequestListeningReceiver,
70                 new IntentFilter(TileService.ACTION_REQUEST_LISTENING));
71         mHandler = new Handler(looper);
72         mMainHandler = new Handler(Looper.getMainLooper());
73     }
74 
getContext()75     public Context getContext() {
76         return mContext;
77     }
78 
getHost()79     public QSTileHost getHost() {
80         return mHost;
81     }
82 
getTileWrapper(CustomTile tile)83     public TileServiceManager getTileWrapper(CustomTile tile) {
84         ComponentName component = tile.getComponent();
85         TileServiceManager service = onCreateTileService(component, tile.getQsTile());
86         synchronized (mServices) {
87             mServices.put(tile, service);
88             mTiles.put(component, tile);
89             mTokenMap.put(service.getToken(), tile);
90         }
91         // Makes sure binding only happens after the maps have been populated
92         service.startLifecycleManagerAndAddTile();
93         return service;
94     }
95 
onCreateTileService(ComponentName component, Tile tile)96     protected TileServiceManager onCreateTileService(ComponentName component, Tile tile) {
97         return new TileServiceManager(this, mHandler, component, tile);
98     }
99 
freeService(CustomTile tile, TileServiceManager service)100     public void freeService(CustomTile tile, TileServiceManager service) {
101         synchronized (mServices) {
102             service.setBindAllowed(false);
103             service.handleDestroy();
104             mServices.remove(tile);
105             mTokenMap.remove(service.getToken());
106             mTiles.remove(tile.getComponent());
107             final String slot = tile.getComponent().getClassName();
108             // TileServices doesn't know how to add more than 1 icon per slot, so remove all
109             mMainHandler.post(() -> mHost.getIconController()
110                     .removeAllIconsForSlot(slot));
111         }
112     }
113 
setMemoryPressure(boolean memoryPressure)114     public void setMemoryPressure(boolean memoryPressure) {
115         mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND;
116         recalculateBindAllowance();
117     }
118 
recalculateBindAllowance()119     public void recalculateBindAllowance() {
120         final ArrayList<TileServiceManager> services;
121         synchronized (mServices) {
122             services = new ArrayList<>(mServices.values());
123         }
124         final int N = services.size();
125         if (N > mMaxBound) {
126             long currentTime = System.currentTimeMillis();
127             // Precalculate the priority of services for binding.
128             for (int i = 0; i < N; i++) {
129                 services.get(i).calculateBindPriority(currentTime);
130             }
131             // Sort them so we can bind the most important first.
132             Collections.sort(services, SERVICE_SORT);
133         }
134         int i;
135         // Allow mMaxBound items to bind.
136         for (i = 0; i < mMaxBound && i < N; i++) {
137             services.get(i).setBindAllowed(true);
138         }
139         // The rest aren't allowed to bind for now.
140         while (i < N) {
141             services.get(i).setBindAllowed(false);
142             i++;
143         }
144     }
145 
verifyCaller(CustomTile tile)146     private void verifyCaller(CustomTile tile) {
147         try {
148             String packageName = tile.getComponent().getPackageName();
149             int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
150                     Binder.getCallingUserHandle().getIdentifier());
151             if (Binder.getCallingUid() != uid) {
152                 throw new SecurityException("Component outside caller's uid");
153             }
154         } catch (PackageManager.NameNotFoundException e) {
155             throw new SecurityException(e);
156         }
157     }
158 
requestListening(ComponentName component)159     private void requestListening(ComponentName component) {
160         synchronized (mServices) {
161             CustomTile customTile = getTileForComponent(component);
162             if (customTile == null) {
163                 Log.d("TileServices", "Couldn't find tile for " + component);
164                 return;
165             }
166             TileServiceManager service = mServices.get(customTile);
167             if (!service.isActiveTile()) {
168                 return;
169             }
170             service.setBindRequested(true);
171             try {
172                 service.getTileService().onStartListening();
173             } catch (RemoteException e) {
174             }
175         }
176     }
177 
178     @Override
updateQsTile(Tile tile, IBinder token)179     public void updateQsTile(Tile tile, IBinder token) {
180         CustomTile customTile = getTileForToken(token);
181         if (customTile != null) {
182             verifyCaller(customTile);
183             synchronized (mServices) {
184                 final TileServiceManager tileServiceManager = mServices.get(customTile);
185                 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) {
186                     Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(),
187                             new IllegalStateException());
188                     return;
189                 }
190                 tileServiceManager.clearPendingBind();
191                 tileServiceManager.setLastUpdate(System.currentTimeMillis());
192             }
193             customTile.updateState(tile);
194             customTile.refreshState();
195         }
196     }
197 
198     @Override
onStartSuccessful(IBinder token)199     public void onStartSuccessful(IBinder token) {
200         CustomTile customTile = getTileForToken(token);
201         if (customTile != null) {
202             verifyCaller(customTile);
203             synchronized (mServices) {
204                 final TileServiceManager tileServiceManager = mServices.get(customTile);
205                 // This should not happen as the TileServiceManager should have been started for the
206                 // first bind to happen.
207                 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) {
208                     Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(),
209                             new IllegalStateException());
210                     return;
211                 }
212                 tileServiceManager.clearPendingBind();
213             }
214             customTile.refreshState();
215         }
216     }
217 
218     @Override
onShowDialog(IBinder token)219     public void onShowDialog(IBinder token) {
220         CustomTile customTile = getTileForToken(token);
221         if (customTile != null) {
222             verifyCaller(customTile);
223             customTile.onDialogShown();
224             mHost.forceCollapsePanels();
225             mServices.get(customTile).setShowingDialog(true);
226         }
227     }
228 
229     @Override
onDialogHidden(IBinder token)230     public void onDialogHidden(IBinder token) {
231         CustomTile customTile = getTileForToken(token);
232         if (customTile != null) {
233             verifyCaller(customTile);
234             mServices.get(customTile).setShowingDialog(false);
235             customTile.onDialogHidden();
236         }
237     }
238 
239     @Override
onStartActivity(IBinder token)240     public void onStartActivity(IBinder token) {
241         CustomTile customTile = getTileForToken(token);
242         if (customTile != null) {
243             verifyCaller(customTile);
244             mHost.forceCollapsePanels();
245         }
246     }
247 
248     @Override
updateStatusIcon(IBinder token, Icon icon, String contentDescription)249     public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
250         CustomTile customTile = getTileForToken(token);
251         if (customTile != null) {
252             verifyCaller(customTile);
253             try {
254                 ComponentName componentName = customTile.getComponent();
255                 String packageName = componentName.getPackageName();
256                 UserHandle userHandle = getCallingUserHandle();
257                 PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0,
258                         userHandle.getIdentifier());
259                 if (info.applicationInfo.isSystemApp()) {
260                     final StatusBarIcon statusIcon = icon != null
261                             ? new StatusBarIcon(userHandle, packageName, icon, 0, 0,
262                                     contentDescription)
263                             : null;
264                     mMainHandler.post(new Runnable() {
265                         @Override
266                         public void run() {
267                             StatusBarIconController iconController = mHost.getIconController();
268                             iconController.setIcon(componentName.getClassName(), statusIcon);
269                             iconController.setExternalIcon(componentName.getClassName());
270                         }
271                     });
272                 }
273             } catch (PackageManager.NameNotFoundException e) {
274             }
275         }
276     }
277 
278     @Override
getTile(IBinder token)279     public Tile getTile(IBinder token) {
280         CustomTile customTile = getTileForToken(token);
281         if (customTile != null) {
282             verifyCaller(customTile);
283             return customTile.getQsTile();
284         }
285         return null;
286     }
287 
288     @Override
startUnlockAndRun(IBinder token)289     public void startUnlockAndRun(IBinder token) {
290         CustomTile customTile = getTileForToken(token);
291         if (customTile != null) {
292             verifyCaller(customTile);
293             customTile.startUnlockAndRun();
294         }
295     }
296 
297     @Override
isLocked()298     public boolean isLocked() {
299         KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class);
300         return keyguardMonitor.isShowing();
301     }
302 
303     @Override
isSecure()304     public boolean isSecure() {
305         KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class);
306         return keyguardMonitor.isSecure() && keyguardMonitor.isShowing();
307     }
308 
getTileForToken(IBinder token)309     private CustomTile getTileForToken(IBinder token) {
310         synchronized (mServices) {
311             return mTokenMap.get(token);
312         }
313     }
314 
getTileForComponent(ComponentName component)315     private CustomTile getTileForComponent(ComponentName component) {
316         synchronized (mServices) {
317             return mTiles.get(component);
318         }
319     }
320 
destroy()321     public void destroy() {
322         synchronized (mServices) {
323             mServices.values().forEach(service -> service.handleDestroy());
324             mContext.unregisterReceiver(mRequestListeningReceiver);
325         }
326     }
327 
328     private final BroadcastReceiver mRequestListeningReceiver = new BroadcastReceiver() {
329         @Override
330         public void onReceive(Context context, Intent intent) {
331             if (TileService.ACTION_REQUEST_LISTENING.equals(intent.getAction())) {
332                 requestListening(
333                         (ComponentName) intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME));
334             }
335         }
336     };
337 
338     private static final Comparator<TileServiceManager> SERVICE_SORT =
339             new Comparator<TileServiceManager>() {
340         @Override
341         public int compare(TileServiceManager left, TileServiceManager right) {
342             return -Integer.compare(left.getBindPriority(), right.getBindPriority());
343         }
344     };
345 }
346