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