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.app.ActivityManager; 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.IBinder; 33 import android.os.RemoteException; 34 import android.provider.Settings; 35 import android.service.quicksettings.IQSTileService; 36 import android.service.quicksettings.Tile; 37 import android.service.quicksettings.TileService; 38 import android.text.TextUtils; 39 import android.text.format.DateUtils; 40 import android.util.Log; 41 import android.view.IWindowManager; 42 import android.view.WindowManagerGlobal; 43 import android.widget.Switch; 44 45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 46 import com.android.systemui.Dependency; 47 import com.android.systemui.plugins.ActivityStarter; 48 import com.android.systemui.plugins.qs.QSTile.State; 49 import com.android.systemui.qs.QSHost; 50 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener; 51 import com.android.systemui.qs.tileimpl.QSTileImpl; 52 53 import java.util.Objects; 54 55 public class CustomTile extends QSTileImpl<State> implements TileChangeListener { 56 public static final String PREFIX = "custom("; 57 58 private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS; 59 60 private static final boolean DEBUG = false; 61 62 // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot. 63 // So instead we have a period of waiting. 64 private static final long UNBIND_DELAY = 30000; 65 66 private final ComponentName mComponent; 67 private final Tile mTile; 68 private final IWindowManager mWindowManager; 69 private final IBinder mToken = new Binder(); 70 private final IQSTileService mService; 71 private final TileServiceManager mServiceManager; 72 private final int mUser; 73 private android.graphics.drawable.Icon mDefaultIcon; 74 private CharSequence mDefaultLabel; 75 76 private final Context mUserContext; 77 78 private boolean mListening; 79 private boolean mIsTokenGranted; 80 private boolean mIsShowingDialog; 81 CustomTile(QSHost host, String action, Context userContext)82 private CustomTile(QSHost host, String action, Context userContext) { 83 super(host); 84 mWindowManager = WindowManagerGlobal.getWindowManagerService(); 85 mComponent = ComponentName.unflattenFromString(action); 86 mTile = new Tile(); 87 mUserContext = userContext; 88 mUser = mUserContext.getUserId(); 89 updateDefaultTileAndIcon(); 90 mServiceManager = host.getTileServices().getTileWrapper(this); 91 if (mServiceManager.isToggleableTile()) { 92 // Replace states with BooleanState 93 resetStates(); 94 } 95 96 mService = mServiceManager.getTileService(); 97 mServiceManager.setTileChangeListener(this); 98 } 99 100 @Override getStaleTimeout()101 protected long getStaleTimeout() { 102 return CUSTOM_STALE_TIMEOUT + DateUtils.MINUTE_IN_MILLIS * mHost.indexOf(getTileSpec()); 103 } 104 updateDefaultTileAndIcon()105 private void updateDefaultTileAndIcon() { 106 try { 107 PackageManager pm = mUserContext.getPackageManager(); 108 int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE; 109 if (isSystemApp(pm)) { 110 flags |= PackageManager.MATCH_DISABLED_COMPONENTS; 111 } 112 113 ServiceInfo info = pm.getServiceInfo(mComponent, flags); 114 int icon = info.icon != 0 ? info.icon 115 : info.applicationInfo.icon; 116 // Update the icon if its not set or is the default icon. 117 boolean updateIcon = mTile.getIcon() == null 118 || iconEquals(mTile.getIcon(), mDefaultIcon); 119 mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon 120 .createWithResource(mComponent.getPackageName(), icon) : null; 121 if (updateIcon) { 122 mTile.setIcon(mDefaultIcon); 123 } 124 // Update the label if there is no label or it is the default label. 125 boolean updateLabel = mTile.getLabel() == null 126 || TextUtils.equals(mTile.getLabel(), mDefaultLabel); 127 mDefaultLabel = info.loadLabel(pm); 128 if (updateLabel) { 129 mTile.setLabel(mDefaultLabel); 130 } 131 } catch (PackageManager.NameNotFoundException e) { 132 mDefaultIcon = null; 133 mDefaultLabel = null; 134 } 135 } 136 isSystemApp(PackageManager pm)137 private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException { 138 return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp(); 139 } 140 141 /** 142 * Compare two icons, only works for resources. 143 */ iconEquals(android.graphics.drawable.Icon icon1, android.graphics.drawable.Icon icon2)144 private boolean iconEquals(android.graphics.drawable.Icon icon1, 145 android.graphics.drawable.Icon icon2) { 146 if (icon1 == icon2) { 147 return true; 148 } 149 if (icon1 == null || icon2 == null) { 150 return false; 151 } 152 if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE 153 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) { 154 return false; 155 } 156 if (icon1.getResId() != icon2.getResId()) { 157 return false; 158 } 159 if (!Objects.equals(icon1.getResPackage(), icon2.getResPackage())) { 160 return false; 161 } 162 return true; 163 } 164 165 @Override onTileChanged(ComponentName tile)166 public void onTileChanged(ComponentName tile) { 167 updateDefaultTileAndIcon(); 168 } 169 170 @Override isAvailable()171 public boolean isAvailable() { 172 return mDefaultIcon != null; 173 } 174 getUser()175 public int getUser() { 176 return mUser; 177 } 178 getComponent()179 public ComponentName getComponent() { 180 return mComponent; 181 } 182 183 @Override populate(LogMaker logMaker)184 public LogMaker populate(LogMaker logMaker) { 185 return super.populate(logMaker).setComponentName(mComponent); 186 } 187 getQsTile()188 public Tile getQsTile() { 189 updateDefaultTileAndIcon(); 190 return mTile; 191 } 192 updateState(Tile tile)193 public void updateState(Tile tile) { 194 mTile.setIcon(tile.getIcon()); 195 mTile.setLabel(tile.getLabel()); 196 mTile.setSubtitle(tile.getSubtitle()); 197 mTile.setContentDescription(tile.getContentDescription()); 198 mTile.setStateDescription(tile.getStateDescription()); 199 mTile.setState(tile.getState()); 200 } 201 onDialogShown()202 public void onDialogShown() { 203 mIsShowingDialog = true; 204 } 205 onDialogHidden()206 public void onDialogHidden() { 207 mIsShowingDialog = false; 208 try { 209 if (DEBUG) Log.d(TAG, "Removing token"); 210 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY); 211 } catch (RemoteException e) { 212 } 213 } 214 215 @Override handleSetListening(boolean listening)216 public void handleSetListening(boolean listening) { 217 super.handleSetListening(listening); 218 if (mListening == listening) return; 219 mListening = listening; 220 221 try { 222 if (listening) { 223 updateDefaultTileAndIcon(); 224 refreshState(); 225 if (!mServiceManager.isActiveTile()) { 226 mServiceManager.setBindRequested(true); 227 mService.onStartListening(); 228 } 229 } else { 230 mService.onStopListening(); 231 if (mIsTokenGranted && !mIsShowingDialog) { 232 try { 233 if (DEBUG) Log.d(TAG, "Removing token"); 234 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY); 235 } catch (RemoteException e) { 236 } 237 mIsTokenGranted = false; 238 } 239 mIsShowingDialog = false; 240 mServiceManager.setBindRequested(false); 241 } 242 } catch (RemoteException e) { 243 // Called through wrapper, won't happen here. 244 } 245 } 246 247 @Override handleDestroy()248 protected void handleDestroy() { 249 super.handleDestroy(); 250 if (mIsTokenGranted) { 251 try { 252 if (DEBUG) Log.d(TAG, "Removing token"); 253 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY); 254 } catch (RemoteException e) { 255 } 256 } 257 mHost.getTileServices().freeService(this, mServiceManager); 258 } 259 260 @Override newTileState()261 public State newTileState() { 262 if (mServiceManager != null && mServiceManager.isToggleableTile()) { 263 return new BooleanState(); 264 } 265 return new State(); 266 } 267 268 @Override getLongClickIntent()269 public Intent getLongClickIntent() { 270 Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES); 271 i.setPackage(mComponent.getPackageName()); 272 i = resolveIntent(i); 273 if (i != null) { 274 i.putExtra(Intent.EXTRA_COMPONENT_NAME, mComponent); 275 i.putExtra(TileService.EXTRA_STATE, mTile.getState()); 276 return i; 277 } 278 return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData( 279 Uri.fromParts("package", mComponent.getPackageName(), null)); 280 } 281 resolveIntent(Intent i)282 private Intent resolveIntent(Intent i) { 283 ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0, 284 ActivityManager.getCurrentUser()); 285 return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES) 286 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null; 287 } 288 289 @Override handleClick()290 protected void handleClick() { 291 if (mTile.getState() == Tile.STATE_UNAVAILABLE) { 292 return; 293 } 294 try { 295 if (DEBUG) Log.d(TAG, "Adding token"); 296 mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY); 297 mIsTokenGranted = true; 298 } catch (RemoteException e) { 299 } 300 try { 301 if (mServiceManager.isActiveTile()) { 302 mServiceManager.setBindRequested(true); 303 mService.onStartListening(); 304 } 305 mService.onClick(mToken); 306 } catch (RemoteException e) { 307 // Called through wrapper, won't happen here. 308 } 309 } 310 311 @Override getTileLabel()312 public CharSequence getTileLabel() { 313 return getState().label; 314 } 315 316 @Override handleUpdateState(State state, Object arg)317 protected void handleUpdateState(State state, Object arg) { 318 int tileState = mTile.getState(); 319 if (mServiceManager.hasPendingBind()) { 320 tileState = Tile.STATE_UNAVAILABLE; 321 } 322 state.state = tileState; 323 Drawable drawable; 324 try { 325 drawable = mTile.getIcon().loadDrawable(mUserContext); 326 } catch (Exception e) { 327 Log.w(TAG, "Invalid icon, forcing into unavailable state"); 328 state.state = Tile.STATE_UNAVAILABLE; 329 drawable = mDefaultIcon.loadDrawable(mUserContext); 330 } 331 332 final Drawable drawableF = drawable; 333 state.iconSupplier = () -> { 334 if (drawableF == null) return null; 335 Drawable.ConstantState cs = drawableF.getConstantState(); 336 if (cs != null) { 337 return new DrawableIcon(cs.newDrawable()); 338 } 339 return null; 340 }; 341 state.label = mTile.getLabel(); 342 343 CharSequence subtitle = mTile.getSubtitle(); 344 if (subtitle != null && subtitle.length() > 0) { 345 state.secondaryLabel = subtitle; 346 } else { 347 state.secondaryLabel = null; 348 } 349 350 if (mTile.getContentDescription() != null) { 351 state.contentDescription = mTile.getContentDescription(); 352 } else { 353 state.contentDescription = state.label; 354 } 355 356 if (mTile.getStateDescription() != null) { 357 state.stateDescription = mTile.getStateDescription(); 358 } else { 359 state.stateDescription = null; 360 } 361 362 if (state instanceof BooleanState) { 363 state.expandedAccessibilityClassName = Switch.class.getName(); 364 ((BooleanState) state).value = (state.state == Tile.STATE_ACTIVE); 365 } 366 367 } 368 369 @Override getMetricsCategory()370 public int getMetricsCategory() { 371 return MetricsEvent.QS_CUSTOM; 372 } 373 374 @Override getMetricsSpec()375 public final String getMetricsSpec() { 376 return mComponent.getPackageName(); 377 } 378 startUnlockAndRun()379 public void startUnlockAndRun() { 380 Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> { 381 try { 382 mService.onUnlockComplete(); 383 } catch (RemoteException e) { 384 } 385 }); 386 } 387 toSpec(ComponentName name)388 public static String toSpec(ComponentName name) { 389 return PREFIX + name.flattenToShortString() + ")"; 390 } 391 getComponentFromSpec(String spec)392 public static ComponentName getComponentFromSpec(String spec) { 393 final String action = spec.substring(PREFIX.length(), spec.length() - 1); 394 if (action.isEmpty()) { 395 throw new IllegalArgumentException("Empty custom tile spec action"); 396 } 397 return ComponentName.unflattenFromString(action); 398 } 399 create(QSHost host, String spec, Context userContext)400 public static CustomTile create(QSHost host, String spec, Context userContext) { 401 if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) { 402 throw new IllegalArgumentException("Bad custom tile spec: " + spec); 403 } 404 final String action = spec.substring(PREFIX.length(), spec.length() - 1); 405 if (action.isEmpty()) { 406 throw new IllegalArgumentException("Empty custom tile spec action"); 407 } 408 return new CustomTile(host, action, userContext); 409 } 410 } 411