| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| package com.android.systemui.qs.external; |
| |
| import android.app.PendingIntent; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.graphics.drawable.Icon; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.service.quicksettings.IQSService; |
| import android.service.quicksettings.Tile; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.SparseArrayMap; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import com.android.internal.statusbar.StatusBarIcon; |
| import com.android.systemui.broadcast.BroadcastDispatcher; |
| import com.android.systemui.dagger.SysUISingleton; |
| import com.android.systemui.dagger.qualifiers.Background; |
| import com.android.systemui.dagger.qualifiers.Main; |
| import com.android.systemui.qs.QSHost; |
| import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository; |
| import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor; |
| import com.android.systemui.settings.UserTracker; |
| import com.android.systemui.statusbar.CommandQueue; |
| import com.android.systemui.statusbar.phone.StatusBarIconController; |
| import com.android.systemui.statusbar.policy.KeyguardStateController; |
| import com.android.systemui.util.concurrency.DelayableExecutor; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Objects; |
| |
| import javax.inject.Inject; |
| import javax.inject.Provider; |
| |
| /** |
| * Runs the day-to-day operations of which tiles should be bound and when. |
| */ |
| @SysUISingleton |
| public class TileServices extends IQSService.Stub { |
| static final int DEFAULT_MAX_BOUND = 3; |
| static final int REDUCED_MAX_BOUND = 1; |
| private static final String TAG = "TileServices"; |
| |
| private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>(); |
| private final SparseArrayMap<ComponentName, CustomTile> mTiles = new SparseArrayMap<>(); |
| private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>(); |
| private final Context mContext; |
| private final Handler mMainHandler; |
| private final Provider<Handler> mHandlerProvider; |
| private final QSHost mHost; |
| private final KeyguardStateController mKeyguardStateController; |
| private final BroadcastDispatcher mBroadcastDispatcher; |
| private final CommandQueue mCommandQueue; |
| private final UserTracker mUserTracker; |
| private final StatusBarIconController mStatusBarIconController; |
| private final PanelInteractor mPanelInteractor; |
| private final CustomTileAddedRepository mCustomTileAddedRepository; |
| private final DelayableExecutor mBackgroundExecutor; |
| |
| private int mMaxBound = DEFAULT_MAX_BOUND; |
| |
| @Inject |
| public TileServices( |
| QSHost host, |
| @Main Provider<Handler> handlerProvider, |
| BroadcastDispatcher broadcastDispatcher, |
| UserTracker userTracker, |
| KeyguardStateController keyguardStateController, |
| CommandQueue commandQueue, |
| StatusBarIconController statusBarIconController, |
| PanelInteractor panelInteractor, |
| CustomTileAddedRepository customTileAddedRepository, |
| @Background DelayableExecutor backgroundExecutor) { |
| mHost = host; |
| mKeyguardStateController = keyguardStateController; |
| mContext = mHost.getContext(); |
| mBroadcastDispatcher = broadcastDispatcher; |
| mHandlerProvider = handlerProvider; |
| mMainHandler = mHandlerProvider.get(); |
| mUserTracker = userTracker; |
| mCommandQueue = commandQueue; |
| mStatusBarIconController = statusBarIconController; |
| mCommandQueue.addCallback(mRequestListeningCallback); |
| mPanelInteractor = panelInteractor; |
| mCustomTileAddedRepository = customTileAddedRepository; |
| mBackgroundExecutor = backgroundExecutor; |
| } |
| |
| public Context getContext() { |
| return mContext; |
| } |
| |
| public QSHost getHost() { |
| return mHost; |
| } |
| |
| public TileServiceManager getTileWrapper(CustomTile tile) { |
| ComponentName component = tile.getComponent(); |
| int userId = tile.getUser(); |
| TileServiceManager service = onCreateTileService(component, mBroadcastDispatcher); |
| synchronized (mServices) { |
| mServices.put(tile, service); |
| mTiles.add(userId, component, tile); |
| mTokenMap.put(service.getToken(), tile); |
| } |
| // Makes sure binding only happens after the maps have been populated |
| service.startLifecycleManagerAndAddTile(); |
| return service; |
| } |
| |
| protected TileServiceManager onCreateTileService(ComponentName component, |
| BroadcastDispatcher broadcastDispatcher) { |
| return new TileServiceManager(this, mHandlerProvider.get(), component, |
| broadcastDispatcher, mUserTracker, mCustomTileAddedRepository, mBackgroundExecutor); |
| } |
| |
| public void freeService(CustomTile tile, TileServiceManager service) { |
| synchronized (mServices) { |
| service.setBindAllowed(false); |
| service.handleDestroy(); |
| mServices.remove(tile); |
| mTokenMap.remove(service.getToken()); |
| mTiles.delete(tile.getUser(), tile.getComponent()); |
| final String slot = getStatusBarIconSlotName(tile.getComponent()); |
| mMainHandler.post(() -> mStatusBarIconController.removeIconForTile(slot)); |
| } |
| } |
| |
| public void setMemoryPressure(boolean memoryPressure) { |
| mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND; |
| recalculateBindAllowance(); |
| } |
| |
| public void recalculateBindAllowance() { |
| final ArrayList<TileServiceManager> services; |
| synchronized (mServices) { |
| services = new ArrayList<>(mServices.values()); |
| } |
| final int N = services.size(); |
| if (N > mMaxBound) { |
| long currentTime = System.currentTimeMillis(); |
| // Precalculate the priority of services for binding. |
| for (int i = 0; i < N; i++) { |
| services.get(i).calculateBindPriority(currentTime); |
| } |
| // Sort them so we can bind the most important first. |
| Collections.sort(services, SERVICE_SORT); |
| } |
| int i; |
| // Allow mMaxBound items to bind. |
| for (i = 0; i < mMaxBound && i < N; i++) { |
| services.get(i).setBindAllowed(true); |
| } |
| // The rest aren't allowed to bind for now. |
| while (i < N) { |
| services.get(i).setBindAllowed(false); |
| i++; |
| } |
| } |
| |
| private void verifyCaller(CustomTile tile) { |
| try { |
| String packageName = tile.getComponent().getPackageName(); |
| int uid = mContext.getPackageManager().getPackageUidAsUser(packageName, |
| Binder.getCallingUserHandle().getIdentifier()); |
| if (Binder.getCallingUid() != uid) { |
| throw new SecurityException("Component outside caller's uid"); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| throw new SecurityException(e); |
| } |
| } |
| |
| private void requestListening(ComponentName component) { |
| synchronized (mServices) { |
| int userId = mUserTracker.getUserId(); |
| CustomTile customTile = getTileForUserAndComponent(userId, component); |
| if (customTile == null) { |
| Log.d(TAG, "Couldn't find tile for " + component + "(" + userId + ")"); |
| return; |
| } |
| TileServiceManager service = mServices.get(customTile); |
| if (service == null) { |
| Log.e( |
| TAG, |
| "No TileServiceManager found in requestListening for tile " |
| + customTile.getTileSpec()); |
| return; |
| } |
| if (!service.isActiveTile()) { |
| return; |
| } |
| service.setBindRequested(true); |
| try { |
| service.getTileService().onStartListening(); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| @Override |
| public void updateQsTile(Tile tile, IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| synchronized (mServices) { |
| final TileServiceManager tileServiceManager = mServices.get(customTile); |
| if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) { |
| Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(), |
| new IllegalStateException()); |
| return; |
| } |
| tileServiceManager.clearPendingBind(); |
| tileServiceManager.setLastUpdate(System.currentTimeMillis()); |
| } |
| customTile.updateTileState(tile); |
| customTile.refreshState(); |
| } |
| } |
| |
| @Override |
| public void onStartSuccessful(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| synchronized (mServices) { |
| final TileServiceManager tileServiceManager = mServices.get(customTile); |
| // This should not happen as the TileServiceManager should have been started for the |
| // first bind to happen. |
| if (tileServiceManager == null || !tileServiceManager.isLifecycleStarted()) { |
| Log.e(TAG, "TileServiceManager not started for " + customTile.getComponent(), |
| new IllegalStateException()); |
| return; |
| } |
| tileServiceManager.clearPendingBind(); |
| } |
| customTile.refreshState(); |
| } |
| } |
| |
| @Override |
| public void onShowDialog(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| customTile.onDialogShown(); |
| mPanelInteractor.forceCollapsePanels(); |
| Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(true); |
| } |
| } |
| |
| @Override |
| public void onDialogHidden(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| Objects.requireNonNull(mServices.get(customTile)).setShowingDialog(false); |
| customTile.onDialogHidden(); |
| } |
| } |
| |
| @Override |
| public void onStartActivity(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| mPanelInteractor.forceCollapsePanels(); |
| } |
| } |
| |
| @Override |
| public void startActivity(IBinder token, PendingIntent pendingIntent) { |
| startActivity(getTileForToken(token), pendingIntent); |
| } |
| |
| @VisibleForTesting |
| protected void startActivity(CustomTile customTile, PendingIntent pendingIntent) { |
| if (customTile != null) { |
| verifyCaller(customTile); |
| customTile.startActivityAndCollapse(pendingIntent); |
| } |
| } |
| |
| @Override |
| public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| try { |
| ComponentName componentName = customTile.getComponent(); |
| String packageName = componentName.getPackageName(); |
| UserHandle userHandle = getCallingUserHandle(); |
| PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0, |
| userHandle.getIdentifier()); |
| if (info.applicationInfo.isSystemApp()) { |
| final StatusBarIcon statusIcon = icon != null |
| ? new StatusBarIcon(userHandle, packageName, icon, 0, 0, |
| contentDescription) |
| : null; |
| final String slot = getStatusBarIconSlotName(componentName); |
| mMainHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| mStatusBarIconController.setIconFromTile(slot, statusIcon); |
| } |
| }); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| } |
| } |
| } |
| |
| @Nullable |
| @Override |
| public Tile getTile(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| return customTile.getQsTile(); |
| } |
| Log.e(TAG, "Tile for token " + token + "not found. " |
| + "Tiles in map: " + availableTileComponents()); |
| return null; |
| } |
| |
| private String availableTileComponents() { |
| StringBuilder sb = new StringBuilder("["); |
| synchronized (mServices) { |
| mTokenMap.forEach((iBinder, customTile) -> |
| sb.append(iBinder.toString()) |
| .append(":") |
| .append(customTile.getComponent().flattenToShortString()) |
| .append(":") |
| .append(customTile.getUser()) |
| .append(",")); |
| } |
| sb.append("]"); |
| return sb.toString(); |
| } |
| |
| @Override |
| public void startUnlockAndRun(IBinder token) { |
| CustomTile customTile = getTileForToken(token); |
| if (customTile != null) { |
| verifyCaller(customTile); |
| customTile.startUnlockAndRun(); |
| } |
| } |
| |
| @Override |
| public boolean isLocked() { |
| return mKeyguardStateController.isShowing(); |
| } |
| |
| @Override |
| public boolean isSecure() { |
| return mKeyguardStateController.isMethodSecure() && mKeyguardStateController.isShowing(); |
| } |
| |
| @Nullable |
| public CustomTile getTileForToken(IBinder token) { |
| synchronized (mServices) { |
| return mTokenMap.get(token); |
| } |
| } |
| |
| @Nullable |
| private CustomTile getTileForUserAndComponent(int userId, ComponentName component) { |
| synchronized (mServices) { |
| return mTiles.get(userId, component); |
| } |
| } |
| |
| public void destroy() { |
| synchronized (mServices) { |
| mServices.values().forEach(service -> service.handleDestroy()); |
| } |
| mCommandQueue.removeCallback(mRequestListeningCallback); |
| } |
| |
| /** Returns the slot name that should be used when adding or removing status bar icons. */ |
| private String getStatusBarIconSlotName(ComponentName componentName) { |
| return componentName.getClassName(); |
| } |
| |
| |
| private final CommandQueue.Callbacks mRequestListeningCallback = new CommandQueue.Callbacks() { |
| @Override |
| public void requestTileServiceListeningState(@NonNull ComponentName componentName) { |
| mMainHandler.post(() -> requestListening(componentName)); |
| } |
| }; |
| |
| private static final Comparator<TileServiceManager> SERVICE_SORT = |
| new Comparator<TileServiceManager>() { |
| @Override |
| public int compare(TileServiceManager left, TileServiceManager right) { |
| return -Integer.compare(left.getBindPriority(), right.getBindPriority()); |
| } |
| }; |
| |
| } |