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.app.PendingIntent;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.pm.PackageInfo;
22 import android.content.pm.PackageManager;
23 import android.graphics.drawable.Icon;
24 import android.os.Binder;
25 import android.os.Handler;
26 import android.os.IBinder;
27 import android.os.RemoteException;
28 import android.os.UserHandle;
29 import android.service.quicksettings.IQSService;
30 import android.service.quicksettings.Tile;
31 import android.util.ArrayMap;
32 import android.util.Log;
33 import android.util.SparseArrayMap;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.Nullable;
37 import androidx.annotation.VisibleForTesting;
38 
39 import com.android.internal.statusbar.StatusBarIcon;
40 import com.android.systemui.broadcast.BroadcastDispatcher;
41 import com.android.systemui.dagger.SysUISingleton;
42 import com.android.systemui.dagger.qualifiers.Background;
43 import com.android.systemui.dagger.qualifiers.Main;
44 import com.android.systemui.qs.QSHost;
45 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
46 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
47 import com.android.systemui.settings.UserTracker;
48 import com.android.systemui.statusbar.CommandQueue;
49 import com.android.systemui.statusbar.phone.StatusBarIconController;
50 import com.android.systemui.statusbar.policy.KeyguardStateController;
51 import com.android.systemui.util.concurrency.DelayableExecutor;
52 
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.Comparator;
56 import java.util.Objects;
57 
58 import javax.inject.Inject;
59 import javax.inject.Provider;
60 
61 /**
62  * Runs the day-to-day operations of which tiles should be bound and when.
63  */
64 @SysUISingleton
65 public class TileServices extends IQSService.Stub {
66     static final int DEFAULT_MAX_BOUND = 3;
67     static final int REDUCED_MAX_BOUND = 1;
68     private static final String TAG = "TileServices";
69 
70     private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>();
71     private final SparseArrayMap<ComponentName, CustomTile> mTiles = new SparseArrayMap<>();
72     private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>();
73     private final Context mContext;
74     private final Handler mMainHandler;
75     private final Provider<Handler> mHandlerProvider;
76     private final QSHost mHost;
77     private final KeyguardStateController mKeyguardStateController;
78     private final BroadcastDispatcher mBroadcastDispatcher;
79     private final CommandQueue mCommandQueue;
80     private final UserTracker mUserTracker;
81     private final StatusBarIconController mStatusBarIconController;
82     private final PanelInteractor mPanelInteractor;
83     private final CustomTileAddedRepository mCustomTileAddedRepository;
84     private final DelayableExecutor mBackgroundExecutor;
85 
86     private int mMaxBound = DEFAULT_MAX_BOUND;
87 
88     @Inject
TileServices( QSHost host, @Main Provider<Handler> handlerProvider, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, KeyguardStateController keyguardStateController, CommandQueue commandQueue, StatusBarIconController statusBarIconController, PanelInteractor panelInteractor, CustomTileAddedRepository customTileAddedRepository, @Background DelayableExecutor backgroundExecutor)89     public TileServices(
90             QSHost host,
91             @Main Provider<Handler> handlerProvider,
92             BroadcastDispatcher broadcastDispatcher,
93             UserTracker userTracker,
94             KeyguardStateController keyguardStateController,
95             CommandQueue commandQueue,
96             StatusBarIconController statusBarIconController,
97             PanelInteractor panelInteractor,
98             CustomTileAddedRepository customTileAddedRepository,
99             @Background DelayableExecutor backgroundExecutor) {
100         mHost = host;
101         mKeyguardStateController = keyguardStateController;
102         mContext = mHost.getContext();
103         mBroadcastDispatcher = broadcastDispatcher;
104         mHandlerProvider = handlerProvider;
105         mMainHandler = mHandlerProvider.get();
106         mUserTracker = userTracker;
107         mCommandQueue = commandQueue;
108         mStatusBarIconController = statusBarIconController;
109         mCommandQueue.addCallback(mRequestListeningCallback);
110         mPanelInteractor = panelInteractor;
111         mCustomTileAddedRepository = customTileAddedRepository;
112         mBackgroundExecutor = backgroundExecutor;
113     }
114 
getContext()115     public Context getContext() {
116         return mContext;
117     }
118 
getHost()119     public QSHost getHost() {
120         return mHost;
121     }
122 
getTileWrapper(CustomTile tile)123     public TileServiceManager getTileWrapper(CustomTile tile) {
124         ComponentName component = tile.getComponent();
125         int userId = tile.getUser();
126         TileServiceManager service = onCreateTileService(component, mBroadcastDispatcher);
127         synchronized (mServices) {
128             mServices.put(tile, service);
129             mTiles.add(userId, component, tile);
130             mTokenMap.put(service.getToken(), tile);
131         }
132         // Makes sure binding only happens after the maps have been populated
133         service.startLifecycleManagerAndAddTile();
134         return service;
135     }
136 
onCreateTileService(ComponentName component, BroadcastDispatcher broadcastDispatcher)137     protected TileServiceManager onCreateTileService(ComponentName component,
138             BroadcastDispatcher broadcastDispatcher) {
139         return new TileServiceManager(this, mHandlerProvider.get(), component,
140                 broadcastDispatcher, mUserTracker, mCustomTileAddedRepository, mBackgroundExecutor);
141     }
142 
freeService(CustomTile tile, TileServiceManager service)143     public void freeService(CustomTile tile, TileServiceManager service) {
144         synchronized (mServices) {
145             service.setBindAllowed(false);
146             service.handleDestroy();
147             mServices.remove(tile);
148             mTokenMap.remove(service.getToken());
149             mTiles.delete(tile.getUser(), tile.getComponent());
150             final String slot = getStatusBarIconSlotName(tile.getComponent());
151             mMainHandler.post(() -> mStatusBarIconController.removeIconForTile(slot));
152         }
153     }
154 
setMemoryPressure(boolean memoryPressure)155     public void setMemoryPressure(boolean memoryPressure) {
156         mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND;
157         recalculateBindAllowance();
158     }
159 
recalculateBindAllowance()160     public void recalculateBindAllowance() {
161         final ArrayList<TileServiceManager> services;
162         synchronized (mServices) {
163             services = new ArrayList<>(mServices.values());
164         }
165         final int N = services.size();
166         if (N > mMaxBound) {
167             long currentTime = System.currentTimeMillis();
168             // Precalculate the priority of services for binding.
169             for (int i = 0; i < N; i++) {
170                 services.get(i).calculateBindPriority(currentTime);
171             }
172             // Sort them so we can bind the most important first.
173             Collections.sort(services, SERVICE_SORT);
174         }
175         int i;
176         // Allow mMaxBound items to bind.
177         for (i = 0; i < mMaxBound && i < N; i++) {
178             services.get(i).setBindAllowed(true);
179         }
180         // The rest aren't allowed to bind for now.
181         while (i < N) {
182             services.get(i).setBindAllowed(false);
183             i++;
184         }
185     }
186 
verifyCaller(CustomTile tile)187     private void verifyCaller(CustomTile tile) {
188         try {
189             String packageName = tile.getComponent().getPackageName();
190             int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
191                     Binder.getCallingUserHandle().getIdentifier());
192             if (Binder.getCallingUid() != uid) {
193                 throw new SecurityException("Component outside caller's uid");
194             }
195         } catch (PackageManager.NameNotFoundException e) {
196             throw new SecurityException(e);
197         }
198     }
199 
requestListening(ComponentName component)200     private void requestListening(ComponentName component) {
201         synchronized (mServices) {
202             int userId = mUserTracker.getUserId();
203             CustomTile customTile = getTileForUserAndComponent(userId, component);
204             if (customTile == null) {
205                 Log.d(TAG, "Couldn't find tile for " + component + "(" + userId + ")");
206                 return;
207             }
208             TileServiceManager service = mServices.get(customTile);
209             if (service == null) {
210                 Log.e(
211                         TAG,
212                         "No TileServiceManager found in requestListening for tile "
213                                 + customTile.getTileSpec());
214                 return;
215             }
216             if (!service.isActiveTile()) {
217                 return;
218             }
219             service.setBindRequested(true);
220             try {
221                 service.getTileService().onStartListening();
222             } catch (RemoteException e) {
223             }
224         }
225     }
226 
227     @Override
updateQsTile(Tile tile, IBinder token)228     public void updateQsTile(Tile tile, IBinder token) {
229         CustomTile customTile = getTileForToken(token);
230         if (customTile != null) {
231             verifyCaller(customTile);
232             synchronized (mServices) {
233                 final TileServiceManager tileServiceManager = mServices.get(customTile);
234                 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) {
235                     Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(),
236                             new IllegalStateException());
237                     return;
238                 }
239                 tileServiceManager.clearPendingBind();
240                 tileServiceManager.setLastUpdate(System.currentTimeMillis());
241             }
242             customTile.updateTileState(tile);
243             customTile.refreshState();
244         }
245     }
246 
247     @Override
onStartSuccessful(IBinder token)248     public void onStartSuccessful(IBinder token) {
249         CustomTile customTile = getTileForToken(token);
250         if (customTile != null) {
251             verifyCaller(customTile);
252             synchronized (mServices) {
253                 final TileServiceManager tileServiceManager = mServices.get(customTile);
254                 // This should not happen as the TileServiceManager should have been started for the
255                 // first bind to happen.
256                 if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) {
257                     Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(),
258                             new IllegalStateException());
259                     return;
260                 }
261                 tileServiceManager.clearPendingBind();
262             }
263             customTile.refreshState();
264         }
265     }
266 
267     @Override
onShowDialog(IBinder token)268     public void onShowDialog(IBinder token) {
269         CustomTile customTile = getTileForToken(token);
270         if (customTile != null) {
271             verifyCaller(customTile);
272             customTile.onDialogShown();
273             mPanelInteractor.forceCollapsePanels();
274             Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(true);
275         }
276     }
277 
278     @Override
onDialogHidden(IBinder token)279     public void onDialogHidden(IBinder token) {
280         CustomTile customTile = getTileForToken(token);
281         if (customTile != null) {
282             verifyCaller(customTile);
283             Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(false);
284             customTile.onDialogHidden();
285         }
286     }
287 
288     @Override
onStartActivity(IBinder token)289     public void onStartActivity(IBinder token) {
290         CustomTile customTile = getTileForToken(token);
291         if (customTile != null) {
292             verifyCaller(customTile);
293             mPanelInteractor.forceCollapsePanels();
294         }
295     }
296 
297     @Override
startActivity(IBinder token, PendingIntent pendingIntent)298     public void startActivity(IBinder token, PendingIntent pendingIntent) {
299         startActivity(getTileForToken(token), pendingIntent);
300     }
301 
302     @VisibleForTesting
startActivity(CustomTile customTile, PendingIntent pendingIntent)303     protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) {
304         if (customTile != null) {
305             verifyCaller(customTile);
306             customTile.startActivityAndCollapse(pendingIntent);
307         }
308     }
309 
310     @Override
updateStatusIcon(IBinder token, Icon icon, String contentDescription)311     public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
312         CustomTile customTile = getTileForToken(token);
313         if (customTile != null) {
314             verifyCaller(customTile);
315             try {
316                 ComponentName componentName = customTile.getComponent();
317                 String packageName = componentName.getPackageName();
318                 UserHandle userHandle = getCallingUserHandle();
319                 PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0,
320                         userHandle.getIdentifier());
321                 if (info.applicationInfo.isSystemApp()) {
322                     final StatusBarIcon statusIcon = icon != null
323                             ? new StatusBarIcon(userHandle, packageName, icon, 0, 0,
324                                     contentDescription)
325                             : null;
326                     final String slot = getStatusBarIconSlotName(componentName);
327                     mMainHandler.post(new Runnable() {
328                         @Override
329                         public void run() {
330                             mStatusBarIconController.setIconFromTile(slot, statusIcon);
331                         }
332                     });
333                 }
334             } catch (PackageManager.NameNotFoundException e) {
335             }
336         }
337     }
338 
339     @Nullable
340     @Override
getTile(IBinder token)341     public Tile getTile(IBinder token) {
342         CustomTile customTile = getTileForToken(token);
343         if (customTile != null) {
344             verifyCaller(customTile);
345             return customTile.getQsTile();
346         }
347         Log.e(TAG, "Tile for token " + token + "not found. "
348                 + "Tiles in map: " + availableTileComponents());
349         return null;
350     }
351 
availableTileComponents()352     private String availableTileComponents() {
353         StringBuilder sb = new StringBuilder("[");
354         synchronized (mServices) {
355             mTokenMap.forEach((iBinder, customTile) ->
356                     sb.append(iBinder.toString())
357                     .append(":")
358                     .append(customTile.getComponent().flattenToShortString())
359                     .append(":")
360                     .append(customTile.getUser())
361                     .append(","));
362         }
363         sb.append("]");
364         return sb.toString();
365     }
366 
367     @Override
startUnlockAndRun(IBinder token)368     public void startUnlockAndRun(IBinder token) {
369         CustomTile customTile = getTileForToken(token);
370         if (customTile != null) {
371             verifyCaller(customTile);
372             customTile.startUnlockAndRun();
373         }
374     }
375 
376     @Override
isLocked()377     public boolean isLocked() {
378         return mKeyguardStateController.isShowing();
379     }
380 
381     @Override
isSecure()382     public boolean isSecure() {
383         return mKeyguardStateController.isMethodSecure() && mKeyguardStateController.isShowing();
384     }
385 
386     @Nullable
getTileForToken(IBinder token)387     public CustomTile getTileForToken(IBinder token) {
388         synchronized (mServices) {
389             return mTokenMap.get(token);
390         }
391     }
392 
393     @Nullable
getTileForUserAndComponent(int userId, ComponentName component)394     private CustomTile getTileForUserAndComponent(int userId, ComponentName component) {
395         synchronized (mServices) {
396             return mTiles.get(userId, component);
397         }
398     }
399 
destroy()400     public void destroy() {
401         synchronized (mServices) {
402             mServices.values().forEach(service -> service.handleDestroy());
403         }
404         mCommandQueue.removeCallback(mRequestListeningCallback);
405     }
406 
407     /** Returns the slot name that should be used when adding or removing status bar icons. */
getStatusBarIconSlotName(ComponentName componentName)408     private String getStatusBarIconSlotName(ComponentName componentName) {
409         return componentName.getClassName();
410     }
411 
412 
413     private final CommandQueue.Callbacks mRequestListeningCallback = new CommandQueue.Callbacks() {
414         @Override
415         public void requestTileServiceListeningState(@NonNull ComponentName componentName) {
416             mMainHandler.post(() -> requestListening(componentName));
417         }
418     };
419 
420     private static final Comparator<TileServiceManager> SERVICE_SORT =
421             new Comparator<TileServiceManager>() {
422         @Override
423         public int compare(TileServiceManager left, TileServiceManager right) {
424             return -Integer.compare(left.getBindPriority(), right.getBindPriority());
425         }
426     };
427 
428 }
429