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