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