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