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