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