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