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