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 android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG; 19 20 import android.app.IUriGrantsManager; 21 import android.app.PendingIntent; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.pm.ServiceInfo; 28 import android.graphics.drawable.Drawable; 29 import android.metrics.LogMaker; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Process; 36 import android.os.RemoteException; 37 import android.provider.Settings; 38 import android.service.quicksettings.IQSTileService; 39 import android.service.quicksettings.Tile; 40 import android.service.quicksettings.TileService; 41 import android.text.format.DateUtils; 42 import android.util.Log; 43 import android.view.IWindowManager; 44 import android.view.WindowManagerGlobal; 45 import android.widget.Button; 46 import android.widget.Switch; 47 48 import androidx.annotation.Nullable; 49 import androidx.annotation.WorkerThread; 50 51 import com.android.internal.jank.InteractionJankMonitor; 52 import com.android.internal.logging.MetricsLogger; 53 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 54 import com.android.systemui.animation.ActivityTransitionAnimator; 55 import com.android.systemui.animation.Expandable; 56 import com.android.systemui.dagger.qualifiers.Background; 57 import com.android.systemui.dagger.qualifiers.Main; 58 import com.android.systemui.plugins.ActivityStarter; 59 import com.android.systemui.plugins.FalsingManager; 60 import com.android.systemui.plugins.qs.QSTile.State; 61 import com.android.systemui.plugins.statusbar.StatusBarStateController; 62 import com.android.systemui.qs.QSHost; 63 import com.android.systemui.qs.QsEventLogger; 64 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener; 65 import com.android.systemui.qs.logging.QSLogger; 66 import com.android.systemui.qs.tileimpl.QSTileImpl; 67 import com.android.systemui.settings.DisplayTracker; 68 69 import dagger.Lazy; 70 import dagger.assisted.Assisted; 71 import dagger.assisted.AssistedFactory; 72 import dagger.assisted.AssistedInject; 73 74 import java.util.Objects; 75 import java.util.concurrent.atomic.AtomicBoolean; 76 77 public class CustomTile extends QSTileImpl<State> implements TileChangeListener, 78 CustomTileInterface { 79 public static final String PREFIX = "custom("; 80 81 private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS; 82 83 private static final boolean DEBUG = false; 84 85 // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot. 86 // So instead we have a period of waiting. 87 private static final long UNBIND_DELAY = 30000; 88 89 private final ComponentName mComponent; 90 private final Tile mTile; 91 private final IWindowManager mWindowManager; 92 private final IBinder mToken = new Binder(); 93 private final IQSTileService mService; 94 private final TileServiceManager mServiceManager; 95 private final int mUser; 96 private final CustomTileStatePersister mCustomTileStatePersister; 97 private final DisplayTracker mDisplayTracker; 98 @Nullable 99 private android.graphics.drawable.Icon mDefaultIcon; 100 @Nullable 101 private CharSequence mDefaultLabel; 102 @Nullable 103 private Expandable mExpandableClicked; 104 105 private final Context mUserContext; 106 107 private boolean mListening; 108 private boolean mIsTokenGranted; 109 private boolean mIsShowingDialog; 110 111 private final TileServiceKey mKey; 112 113 private final AtomicBoolean mInitialDefaultIconFetched = new AtomicBoolean(false); 114 private final TileServices mTileServices; 115 116 private int mServiceUid = Process.INVALID_UID; 117 118 private final IUriGrantsManager mIUriGrantsManager; 119 120 @AssistedInject CustomTile( Lazy<QSHost> host, QsEventLogger uiEventLogger, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, @Assisted String action, @Assisted Context userContext, CustomTileStatePersister customTileStatePersister, TileServices tileServices, DisplayTracker displayTracker, IUriGrantsManager uriGrantsManager )121 CustomTile( 122 Lazy<QSHost> host, 123 QsEventLogger uiEventLogger, 124 @Background Looper backgroundLooper, 125 @Main Handler mainHandler, 126 FalsingManager falsingManager, 127 MetricsLogger metricsLogger, 128 StatusBarStateController statusBarStateController, 129 ActivityStarter activityStarter, 130 QSLogger qsLogger, 131 @Assisted String action, 132 @Assisted Context userContext, 133 CustomTileStatePersister customTileStatePersister, 134 TileServices tileServices, 135 DisplayTracker displayTracker, 136 IUriGrantsManager uriGrantsManager 137 ) { 138 super(host.get(), uiEventLogger, backgroundLooper, mainHandler, falsingManager, 139 metricsLogger, statusBarStateController, activityStarter, qsLogger); 140 mTileServices = tileServices; 141 mWindowManager = WindowManagerGlobal.getWindowManagerService(); 142 mComponent = ComponentName.unflattenFromString(action); 143 mTile = new Tile(); 144 mUserContext = userContext; 145 mUser = mUserContext.getUserId(); 146 mKey = new TileServiceKey(mComponent, mUser); 147 148 mServiceManager = tileServices.getTileWrapper(this); 149 mService = mServiceManager.getTileService(); 150 mCustomTileStatePersister = customTileStatePersister; 151 mDisplayTracker = displayTracker; 152 mIUriGrantsManager = uriGrantsManager; 153 } 154 155 @Override handleInitialize()156 protected void handleInitialize() { 157 updateDefaultTileAndIcon(); 158 if (mInitialDefaultIconFetched.compareAndSet(false, true)) { 159 if (mDefaultIcon == null) { 160 Log.w(TAG, "No default icon for " + getTileSpec() + ", destroying tile"); 161 mHost.removeTile(getTileSpec()); 162 } 163 } 164 if (mServiceManager.isToggleableTile()) { 165 // Replace states with BooleanState 166 resetStates(); 167 } 168 mServiceManager.setTileChangeListener(this); 169 if (mServiceManager.isActiveTile()) { 170 Tile t = mCustomTileStatePersister.readState(mKey); 171 if (t != null) { 172 applyTileState(t, /* overwriteNulls */ false); 173 mServiceManager.clearPendingBind(); 174 refreshState(); 175 } 176 } 177 } 178 179 @Override getStaleTimeout()180 protected long getStaleTimeout() { 181 return CUSTOM_STALE_TIMEOUT + DateUtils.MINUTE_IN_MILLIS * mHost.indexOf(getTileSpec()); 182 } 183 updateDefaultTileAndIcon()184 private void updateDefaultTileAndIcon() { 185 try { 186 PackageManager pm = mUserContext.getPackageManager(); 187 int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE 188 | PackageManager.MATCH_DIRECT_BOOT_AWARE; 189 if (isSystemApp(pm)) { 190 flags |= PackageManager.MATCH_DISABLED_COMPONENTS; 191 } 192 193 ServiceInfo info = pm.getServiceInfo(mComponent, flags); 194 int icon = info.icon != 0 ? info.icon 195 : info.applicationInfo.icon; 196 // Update the icon if its not set or is the default icon. 197 boolean updateIcon = mTile.getIcon() == null 198 || iconEquals(mTile.getIcon(), mDefaultIcon); 199 mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon 200 .createWithResource(mComponent.getPackageName(), icon) : null; 201 if (updateIcon) { 202 mTile.setIcon(mDefaultIcon); 203 } 204 mDefaultLabel = info.loadLabel(pm); 205 mTile.setDefaultLabel(mDefaultLabel); 206 } catch (PackageManager.NameNotFoundException e) { 207 mDefaultIcon = null; 208 mDefaultLabel = null; 209 } 210 } 211 isSystemApp(PackageManager pm)212 private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException { 213 return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp(); 214 } 215 216 /** 217 * Compare two icons, only works for resources. 218 */ iconEquals(@ullable android.graphics.drawable.Icon icon1, @Nullable android.graphics.drawable.Icon icon2)219 private boolean iconEquals(@Nullable android.graphics.drawable.Icon icon1, 220 @Nullable android.graphics.drawable.Icon icon2) { 221 if (icon1 == icon2) { 222 return true; 223 } 224 if (icon1 == null || icon2 == null) { 225 return false; 226 } 227 if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE 228 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) { 229 return false; 230 } 231 if (icon1.getResId() != icon2.getResId()) { 232 return false; 233 } 234 if (!Objects.equals(icon1.getResPackage(), icon2.getResPackage())) { 235 return false; 236 } 237 return true; 238 } 239 240 @Override onTileChanged(ComponentName tile)241 public void onTileChanged(ComponentName tile) { 242 mHandler.post(this::updateDefaultTileAndIcon); 243 } 244 245 /** 246 * Custom tile is considered available if there is a default icon (obtained from PM). 247 * <p> 248 * It will return {@code true} before initialization, so tiles are not destroyed prematurely. 249 */ 250 @Override isAvailable()251 public boolean isAvailable() { 252 if (mInitialDefaultIconFetched.get()) { 253 return mDefaultIcon != null; 254 } else { 255 return true; 256 } 257 } 258 259 @Override getUser()260 public int getUser() { 261 return mUser; 262 } 263 264 @Override getComponent()265 public ComponentName getComponent() { 266 return mComponent; 267 } 268 269 @Override populate(LogMaker logMaker)270 public LogMaker populate(LogMaker logMaker) { 271 return super.populate(logMaker).setComponentName(mComponent); 272 } 273 274 @Override getQsTile()275 public Tile getQsTile() { 276 // TODO(b/191145007) Move to background thread safely 277 updateDefaultTileAndIcon(); 278 return mTile; 279 } 280 281 /** 282 * Update state of {@link this#mTile} from a remote {@link TileService}. 283 * 284 * @param tile tile populated with state to apply 285 */ 286 @Override updateTileState(Tile tile, int appUid)287 public void updateTileState(Tile tile, int appUid) { 288 mServiceUid = appUid; 289 // This comes from a binder call IQSService.updateQsTile 290 mHandler.post(() -> handleUpdateTileState(tile)); 291 } 292 handleUpdateTileState(Tile tile)293 private void handleUpdateTileState(Tile tile) { 294 applyTileState(tile, /* overwriteNulls */ true); 295 if (mServiceManager.isActiveTile()) { 296 mCustomTileStatePersister.persistState(mKey, tile); 297 } 298 } 299 300 @WorkerThread applyTileState(Tile tile, boolean overwriteNulls)301 private void applyTileState(Tile tile, boolean overwriteNulls) { 302 if (tile.getIcon() != null || overwriteNulls) { 303 mTile.setIcon(tile.getIcon()); 304 } 305 if (tile.getCustomLabel() != null || overwriteNulls) { 306 mTile.setLabel(tile.getCustomLabel()); 307 } 308 if (tile.getSubtitle() != null || overwriteNulls) { 309 mTile.setSubtitle(tile.getSubtitle()); 310 } 311 if (tile.getContentDescription() != null || overwriteNulls) { 312 mTile.setContentDescription(tile.getContentDescription()); 313 } 314 if (tile.getStateDescription() != null || overwriteNulls) { 315 mTile.setStateDescription(tile.getStateDescription()); 316 } 317 mTile.setActivityLaunchForClick(tile.getActivityLaunchForClick()); 318 mTile.setState(tile.getState()); 319 } 320 321 @Override onDialogShown()322 public void onDialogShown() { 323 mIsShowingDialog = true; 324 } 325 326 @Override onDialogHidden()327 public void onDialogHidden() { 328 mIsShowingDialog = false; 329 try { 330 if (DEBUG) Log.d(TAG, "Removing token"); 331 mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId()); 332 } catch (RemoteException e) { 333 } 334 } 335 336 @Override handleSetListening(boolean listening)337 public void handleSetListening(boolean listening) { 338 super.handleSetListening(listening); 339 if (mListening == listening) return; 340 mListening = listening; 341 342 try { 343 if (listening) { 344 updateDefaultTileAndIcon(); 345 refreshState(); 346 if (!mServiceManager.isActiveTile() || !isTileReady()) { 347 mServiceManager.setBindRequested(true); 348 mService.onStartListening(); 349 } 350 } else { 351 mExpandableClicked = null; 352 mService.onStopListening(); 353 if (mIsTokenGranted && !mIsShowingDialog) { 354 try { 355 if (DEBUG) Log.d(TAG, "Removing token"); 356 mWindowManager.removeWindowToken(mToken, 357 mDisplayTracker.getDefaultDisplayId()); 358 } catch (RemoteException e) { 359 } 360 mIsTokenGranted = false; 361 } 362 mIsShowingDialog = false; 363 mServiceManager.setBindRequested(false); 364 } 365 } catch (RemoteException e) { 366 // Called through wrapper, won't happen here. 367 } 368 } 369 370 @Override handleDestroy()371 protected void handleDestroy() { 372 super.handleDestroy(); 373 if (mIsTokenGranted) { 374 try { 375 if (DEBUG) Log.d(TAG, "Removing token"); 376 mWindowManager.removeWindowToken(mToken, mDisplayTracker.getDefaultDisplayId()); 377 } catch (RemoteException e) { 378 } 379 } 380 mTileServices.freeService(this, mServiceManager); 381 } 382 383 @Override newTileState()384 public State newTileState() { 385 if (mServiceManager != null && mServiceManager.isToggleableTile()) { 386 return new BooleanState(); 387 } 388 return new State(); 389 } 390 391 @Override getLongClickIntent()392 public Intent getLongClickIntent() { 393 Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES); 394 i.setPackage(mComponent.getPackageName()); 395 i = resolveIntent(i); 396 if (i != null) { 397 i.putExtra(Intent.EXTRA_COMPONENT_NAME, mComponent); 398 i.putExtra(TileService.EXTRA_STATE, mTile.getState()); 399 return i; 400 } 401 return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData( 402 Uri.fromParts("package", mComponent.getPackageName(), null)); 403 } 404 405 @Nullable resolveIntent(Intent i)406 private Intent resolveIntent(Intent i) { 407 ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0, mUser); 408 return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES) 409 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null; 410 } 411 412 @Override handleClick(@ullable Expandable expandable)413 protected void handleClick(@Nullable Expandable expandable) { 414 if (mTile.getState() == Tile.STATE_UNAVAILABLE) { 415 return; 416 } 417 mExpandableClicked = expandable; 418 try { 419 if (DEBUG) Log.d(TAG, "Adding token"); 420 mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, 421 mDisplayTracker.getDefaultDisplayId(), null /* options */); 422 mIsTokenGranted = true; 423 } catch (RemoteException e) { 424 } 425 try { 426 if (mServiceManager.isActiveTile()) { 427 mServiceManager.setBindRequested(true); 428 mService.onStartListening(); 429 } 430 431 if (mTile.getActivityLaunchForClick() != null) { 432 startActivityAndCollapse(mTile.getActivityLaunchForClick()); 433 } else { 434 mService.onClick(mToken); 435 } 436 } catch (RemoteException e) { 437 // Called through wrapper, won't happen here. 438 } 439 } 440 441 @Override getTileLabel()442 public CharSequence getTileLabel() { 443 return getState().label; 444 } 445 446 @Override handleUpdateState(State state, Object arg)447 protected void handleUpdateState(State state, Object arg) { 448 int tileState = mTile.getState(); 449 if (mServiceManager.hasPendingBind()) { 450 tileState = Tile.STATE_UNAVAILABLE; 451 } 452 state.state = tileState; 453 Drawable drawable = null; 454 try { 455 drawable = mTile.getIcon().loadDrawableCheckingUriGrant( 456 mUserContext, 457 mIUriGrantsManager, 458 mServiceUid, 459 mComponent.getPackageName() 460 ); 461 } catch (Exception e) { 462 Log.w(TAG, "Invalid icon, forcing into unavailable state"); 463 state.state = Tile.STATE_UNAVAILABLE; 464 } 465 466 final Drawable drawableF; 467 if (drawable != null) { 468 drawableF = drawable; 469 } else if (mDefaultIcon != null) { 470 drawableF = mDefaultIcon.loadDrawable(mUserContext); 471 } else { 472 drawableF = null; 473 } 474 state.iconSupplier = () -> { 475 if (drawableF == null) return null; 476 Drawable.ConstantState cs = drawableF.getConstantState(); 477 if (cs != null) { 478 return new DrawableIcon(cs.newDrawable()); 479 } 480 return null; 481 }; 482 state.label = mTile.getLabel(); 483 484 CharSequence subtitle = mTile.getSubtitle(); 485 if (subtitle != null && subtitle.length() > 0) { 486 state.secondaryLabel = subtitle; 487 } else { 488 state.secondaryLabel = null; 489 } 490 491 if (mTile.getContentDescription() != null) { 492 state.contentDescription = mTile.getContentDescription(); 493 } else { 494 state.contentDescription = state.label; 495 } 496 497 if (mTile.getStateDescription() != null) { 498 state.stateDescription = mTile.getStateDescription(); 499 } else { 500 state.stateDescription = null; 501 } 502 503 if (state instanceof BooleanState) { 504 state.expandedAccessibilityClassName = Switch.class.getName(); 505 ((BooleanState) state).value = (state.state == Tile.STATE_ACTIVE); 506 } else { 507 state.expandedAccessibilityClassName = Button.class.getName(); 508 } 509 510 } 511 512 @Override getMetricsCategory()513 public int getMetricsCategory() { 514 return MetricsEvent.QS_CUSTOM; 515 } 516 517 @Override getMetricsSpec()518 public final String getMetricsSpec() { 519 return mComponent.getPackageName(); 520 } 521 522 @Override startUnlockAndRun()523 public void startUnlockAndRun() { 524 mActivityStarter.postQSRunnableDismissingKeyguard(() -> { 525 try { 526 mService.onUnlockComplete(); 527 } catch (RemoteException e) { 528 } 529 }); 530 } 531 532 /** 533 * Starts an {@link android.app.Activity} 534 * 535 * @param pendingIntent A PendingIntent for an Activity to be launched immediately. 536 */ 537 @Override startActivityAndCollapse(PendingIntent pendingIntent)538 public void startActivityAndCollapse(PendingIntent pendingIntent) { 539 if (!pendingIntent.isActivity()) { 540 Log.i(TAG, "Intent not for activity."); 541 } else if (!mIsTokenGranted) { 542 Log.i(TAG, "Launching activity before click"); 543 } else { 544 Log.i(TAG, "The activity is starting"); 545 546 ActivityTransitionAnimator.Controller controller = 547 mExpandableClicked == null ? null : 548 mExpandableClicked.activityTransitionController( 549 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE); 550 mActivityStarter.startPendingIntentMaybeDismissingKeyguard( 551 pendingIntent, 552 /* intentSentUiThreadCallback= */ null, 553 controller 554 ); 555 } 556 } 557 toSpec(ComponentName name)558 public static String toSpec(ComponentName name) { 559 return PREFIX + name.flattenToShortString() + ")"; 560 } 561 getComponentFromSpec(String spec)562 public static ComponentName getComponentFromSpec(String spec) { 563 final String action = spec.substring(PREFIX.length(), spec.length() - 1); 564 if (action.isEmpty()) { 565 throw new IllegalArgumentException("Empty custom tile spec action"); 566 } 567 return ComponentName.unflattenFromString(action); 568 } 569 getAction(String spec)570 private static String getAction(String spec) { 571 if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) { 572 throw new IllegalArgumentException("Bad custom tile spec: " + spec); 573 } 574 final String action = spec.substring(PREFIX.length(), spec.length() - 1); 575 if (action.isEmpty()) { 576 throw new IllegalArgumentException("Empty custom tile spec action"); 577 } 578 return action; 579 } 580 581 /** 582 * Create a {@link CustomTile} for a given spec and user. 583 * 584 * @param factory including injected common dependencies. 585 * @param spec as provided by {@link CustomTile#toSpec} 586 * @param userContext context for the user that is creating this tile. 587 * @return a new {@link CustomTile} 588 */ create(Factory factory, String spec, Context userContext)589 public static CustomTile create(Factory factory, String spec, Context userContext) { 590 return factory.create(getAction(spec), userContext); 591 } 592 593 @AssistedFactory 594 public interface Factory { create(String action, Context userContext)595 CustomTile create(String action, Context userContext); 596 } 597 } 598