blob: 82c0b79492b3f8dc095d0e51895691a69529db71 [file] [log] [blame]
Jason Monk5db8a412015-10-21 15:16:23 -07001/*
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
Jason Monkd5a204f2015-12-21 08:50:01 -050014 * limitations under the License
Jason Monk5db8a412015-10-21 15:16:23 -070015 */
Jason Monkd5a204f2015-12-21 08:50:01 -050016package com.android.systemui.qs.external;
Jason Monk5db8a412015-10-21 15:16:23 -070017
Jason Monk724214a2016-02-19 16:43:00 -050018import android.app.ActivityManager;
Jason Monk5db8a412015-10-21 15:16:23 -070019import android.content.ComponentName;
Jason Monkd2274f82016-12-12 12:02:16 -050020import android.content.Context;
Jason Monk76c67aa2016-02-19 14:49:42 -050021import android.content.Intent;
Jason Monk5db8a412015-10-21 15:16:23 -070022import android.content.pm.PackageManager;
Jason Monkd2274f82016-12-12 12:02:16 -050023import android.content.pm.PackageManager.NameNotFoundException;
Jason Monk724214a2016-02-19 16:43:00 -050024import android.content.pm.ResolveInfo;
Jason Monk5db8a412015-10-21 15:16:23 -070025import android.content.pm.ServiceInfo;
Jason Monk068cb8b2015-12-02 11:30:36 -050026import android.graphics.drawable.Drawable;
Jason Monkd2274f82016-12-12 12:02:16 -050027import android.graphics.drawable.Icon;
Jason Monk76c67aa2016-02-19 14:49:42 -050028import android.net.Uri;
Jason Monk8f7f3182015-11-18 16:35:14 -050029import android.os.Binder;
Jason Monkbbadff82015-11-06 15:47:26 -050030import android.os.IBinder;
Jason Monk8f7f3182015-11-18 16:35:14 -050031import android.os.RemoteException;
Jason Monk76c67aa2016-02-19 14:49:42 -050032import android.provider.Settings;
Jason Monkbbadff82015-11-06 15:47:26 -050033import android.service.quicksettings.IQSTileService;
34import android.service.quicksettings.Tile;
Jason Monkfe8f6822015-12-21 15:12:01 -050035import android.service.quicksettings.TileService;
Jason Monk94295132016-01-12 11:27:02 -050036import android.text.SpannableStringBuilder;
37import android.text.style.ForegroundColorSpan;
Jason Monkbbadff82015-11-06 15:47:26 -050038import android.util.Log;
Jason Monk8f7f3182015-11-18 16:35:14 -050039import android.view.IWindowManager;
40import android.view.WindowManager;
41import android.view.WindowManagerGlobal;
Jason Monk5db8a412015-10-21 15:16:23 -070042import com.android.internal.logging.MetricsLogger;
Chris Wrenf6e9228b2016-01-26 18:04:35 -050043import com.android.internal.logging.MetricsProto.MetricsEvent;
Jason Monk94295132016-01-12 11:27:02 -050044import com.android.systemui.R;
Jason Monk5db8a412015-10-21 15:16:23 -070045import com.android.systemui.qs.QSTile;
Jason Monk624cbe22016-05-02 10:42:17 -040046import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
Jason Monkbbadff82015-11-06 15:47:26 -050047import com.android.systemui.statusbar.phone.QSTileHost;
Jason Monk624cbe22016-05-02 10:42:17 -040048import libcore.util.Objects;
Jason Monk5db8a412015-10-21 15:16:23 -070049
Jason Monk624cbe22016-05-02 10:42:17 -040050public class CustomTile extends QSTile<QSTile.State> implements TileChangeListener {
Jason Monk5db8a412015-10-21 15:16:23 -070051 public static final String PREFIX = "custom(";
52
Jason Monk8f7f3182015-11-18 16:35:14 -050053 private static final boolean DEBUG = false;
54
Jason Monkbbadff82015-11-06 15:47:26 -050055 // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
56 // So instead we have a period of waiting.
57 private static final long UNBIND_DELAY = 30000;
Jason Monk5db8a412015-10-21 15:16:23 -070058
Jason Monkbbadff82015-11-06 15:47:26 -050059 private final ComponentName mComponent;
60 private final Tile mTile;
Jason Monk8f7f3182015-11-18 16:35:14 -050061 private final IWindowManager mWindowManager;
62 private final IBinder mToken = new Binder();
Jason Monkd5a204f2015-12-21 08:50:01 -050063 private final IQSTileService mService;
64 private final TileServiceManager mServiceManager;
Jason Monk1ffa11b2016-03-08 14:44:23 -050065 private final int mUser;
Jason Monkd2274f82016-12-12 12:02:16 -050066 private Context mAppContext;
Jason Monk624cbe22016-05-02 10:42:17 -040067 private android.graphics.drawable.Icon mDefaultIcon;
Jason Monkbbadff82015-11-06 15:47:26 -050068
Jason Monkbbadff82015-11-06 15:47:26 -050069 private boolean mListening;
70 private boolean mBound;
Jason Monk8f7f3182015-11-18 16:35:14 -050071 private boolean mIsTokenGranted;
72 private boolean mIsShowingDialog;
Jason Monkbbadff82015-11-06 15:47:26 -050073
74 private CustomTile(QSTileHost host, String action) {
Jason Monk5db8a412015-10-21 15:16:23 -070075 super(host);
Jason Monk8f7f3182015-11-18 16:35:14 -050076 mWindowManager = WindowManagerGlobal.getWindowManagerService();
Jason Monk5db8a412015-10-21 15:16:23 -070077 mComponent = ComponentName.unflattenFromString(action);
Jason Monkee68fd82016-06-23 13:12:23 -040078 mTile = new Tile();
Jason Monka3453b8b2016-06-17 12:42:59 -040079 setTileIcon();
Jason Monkd5a204f2015-12-21 08:50:01 -050080 mServiceManager = host.getTileServices().getTileWrapper(this);
81 mService = mServiceManager.getTileService();
Jason Monk624cbe22016-05-02 10:42:17 -040082 mServiceManager.setTileChangeListener(this);
Jason Monk1ffa11b2016-03-08 14:44:23 -050083 mUser = ActivityManager.getCurrentUser();
Jason Monkd2274f82016-12-12 12:02:16 -050084 try {
85 mAppContext = mContext.createPackageContext(mComponent.getPackageName(), 0);
86 } catch (NameNotFoundException e) {
87 }
Jason Monk5db8a412015-10-21 15:16:23 -070088 }
89
Jason Monk624cbe22016-05-02 10:42:17 -040090 private void setTileIcon() {
91 try {
92 PackageManager pm = mContext.getPackageManager();
Will Harmon294af232016-06-24 17:02:34 -070093 int flags = PackageManager.MATCH_ENCRYPTION_AWARE_AND_UNAWARE;
94 if (isSystemApp(pm)) {
95 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
96 }
97 ServiceInfo info = pm.getServiceInfo(mComponent, flags);
Jason Monka5f6ed32016-06-22 09:58:46 -040098 int icon = info.icon != 0 ? info.icon
99 : info.applicationInfo.icon;
Jason Monk624cbe22016-05-02 10:42:17 -0400100 // Update the icon if its not set or is the default icon.
101 boolean updateIcon = mTile.getIcon() == null
102 || iconEquals(mTile.getIcon(), mDefaultIcon);
Jason Monka5f6ed32016-06-22 09:58:46 -0400103 mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
104 .createWithResource(mComponent.getPackageName(), icon) : null;
Jason Monk624cbe22016-05-02 10:42:17 -0400105 if (updateIcon) {
106 mTile.setIcon(mDefaultIcon);
107 }
108 // Update the label if there is no label.
109 if (mTile.getLabel() == null) {
110 mTile.setLabel(info.loadLabel(pm));
111 }
112 } catch (Exception e) {
113 mDefaultIcon = null;
114 }
115 }
116
Will Harmon294af232016-06-24 17:02:34 -0700117 private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
118 return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
119 }
120
Jason Monk624cbe22016-05-02 10:42:17 -0400121 /**
122 * Compare two icons, only works for resources.
123 */
124 private boolean iconEquals(android.graphics.drawable.Icon icon1,
125 android.graphics.drawable.Icon icon2) {
126 if (icon1 == icon2) {
127 return true;
128 }
129 if (icon1 == null || icon2 == null) {
130 return false;
131 }
132 if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
133 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
134 return false;
135 }
136 if (icon1.getResId() != icon2.getResId()) {
137 return false;
138 }
139 if (!Objects.equal(icon1.getResPackage(), icon2.getResPackage())) {
140 return false;
141 }
142 return true;
143 }
144
145 @Override
146 public void onTileChanged(ComponentName tile) {
147 setTileIcon();
148 }
149
Jason Monk1c2fea82016-03-11 11:33:36 -0500150 @Override
151 public boolean isAvailable() {
Jason Monk4a906f92016-04-20 10:54:55 -0400152 return mDefaultIcon != null;
Jason Monk1c2fea82016-03-11 11:33:36 -0500153 }
154
Jason Monk1ffa11b2016-03-08 14:44:23 -0500155 public int getUser() {
156 return mUser;
157 }
158
Jason Monkbbadff82015-11-06 15:47:26 -0500159 public ComponentName getComponent() {
160 return mComponent;
161 }
162
163 public Tile getQsTile() {
164 return mTile;
165 }
166
167 public void updateState(Tile tile) {
Jason Monkbbadff82015-11-06 15:47:26 -0500168 mTile.setIcon(tile.getIcon());
169 mTile.setLabel(tile.getLabel());
170 mTile.setContentDescription(tile.getContentDescription());
Jason Monk94295132016-01-12 11:27:02 -0500171 mTile.setState(tile.getState());
Jason Monk5db8a412015-10-21 15:16:23 -0700172 }
173
Jason Monk8f7f3182015-11-18 16:35:14 -0500174 public void onDialogShown() {
175 mIsShowingDialog = true;
176 }
177
Jason Monk34a5cef2016-01-29 11:28:44 -0500178 public void onDialogHidden() {
179 mIsShowingDialog = false;
180 try {
181 if (DEBUG) Log.d(TAG, "Removing token");
182 mWindowManager.removeWindowToken(mToken);
183 } catch (RemoteException e) {
184 }
185 }
186
Jason Monk5db8a412015-10-21 15:16:23 -0700187 @Override
188 public void setListening(boolean listening) {
Jason Monkbbadff82015-11-06 15:47:26 -0500189 if (mListening == listening) return;
190 mListening = listening;
Jason Monkd5a204f2015-12-21 08:50:01 -0500191 try {
192 if (listening) {
Jason Monk624cbe22016-05-02 10:42:17 -0400193 setTileIcon();
194 refreshState();
Jason Monk97d22722016-04-07 11:41:47 -0400195 if (!mServiceManager.isActiveTile()) {
Jason Monkfe8f6822015-12-21 15:12:01 -0500196 mServiceManager.setBindRequested(true);
197 mService.onStartListening();
198 }
Jason Monkdc35dcb2015-12-04 16:36:15 -0500199 } else {
Jason Monkbbadff82015-11-06 15:47:26 -0500200 mService.onStopListening();
Jason Monkd5a204f2015-12-21 08:50:01 -0500201 if (mIsTokenGranted && !mIsShowingDialog) {
202 try {
203 if (DEBUG) Log.d(TAG, "Removing token");
204 mWindowManager.removeWindowToken(mToken);
205 } catch (RemoteException e) {
206 }
207 mIsTokenGranted = false;
Jason Monk8f7f3182015-11-18 16:35:14 -0500208 }
Jason Monkd5a204f2015-12-21 08:50:01 -0500209 mIsShowingDialog = false;
210 mServiceManager.setBindRequested(false);
Jason Monk8f7f3182015-11-18 16:35:14 -0500211 }
Jason Monkd5a204f2015-12-21 08:50:01 -0500212 } catch (RemoteException e) {
213 // Called through wrapper, won't happen here.
Jason Monkbbadff82015-11-06 15:47:26 -0500214 }
215 }
Jason Monk8f7f3182015-11-18 16:35:14 -0500216
Jason Monkbbadff82015-11-06 15:47:26 -0500217 @Override
218 protected void handleDestroy() {
219 super.handleDestroy();
Jason Monk8f7f3182015-11-18 16:35:14 -0500220 if (mIsTokenGranted) {
221 try {
222 if (DEBUG) Log.d(TAG, "Removing token");
223 mWindowManager.removeWindowToken(mToken);
224 } catch (RemoteException e) {
225 }
226 }
Jason Monk66c89c12016-01-06 08:51:26 -0500227 mHost.getTileServices().freeService(this, mServiceManager);
Jason Monk5db8a412015-10-21 15:16:23 -0700228 }
229
230 @Override
Jason Monk62b63a02016-02-02 15:15:31 -0500231 public State newTileState() {
Yoshinori Hiranoeb093622016-08-17 14:09:58 +0900232 State state = new State();
233 state.autoMirrorDrawable = false;
234 return state;
Jason Monk5db8a412015-10-21 15:16:23 -0700235 }
236
237 @Override
Jason Monk76c67aa2016-02-19 14:49:42 -0500238 public Intent getLongClickIntent() {
Jason Monk724214a2016-02-19 16:43:00 -0500239 Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
240 i.setPackage(mComponent.getPackageName());
241 i = resolveIntent(i);
242 if (i != null) {
243 return i;
244 }
Jason Monk76c67aa2016-02-19 14:49:42 -0500245 return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
246 Uri.fromParts("package", mComponent.getPackageName(), null));
Jason Monk5db8a412015-10-21 15:16:23 -0700247 }
248
Jason Monk724214a2016-02-19 16:43:00 -0500249 private Intent resolveIntent(Intent i) {
250 ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
251 ActivityManager.getCurrentUser());
252 return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
253 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
254 }
255
Jason Monk5db8a412015-10-21 15:16:23 -0700256 @Override
257 protected void handleClick() {
Jason Monk94295132016-01-12 11:27:02 -0500258 if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
259 return;
260 }
Jason Monkfe8f6822015-12-21 15:12:01 -0500261 try {
262 if (DEBUG) Log.d(TAG, "Adding token");
263 mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
264 mIsTokenGranted = true;
265 } catch (RemoteException e) {
266 }
267 try {
Jason Monk97d22722016-04-07 11:41:47 -0400268 if (mServiceManager.isActiveTile()) {
Jason Monkfe8f6822015-12-21 15:12:01 -0500269 mServiceManager.setBindRequested(true);
270 mService.onStartListening();
Jason Monk8f7f3182015-11-18 16:35:14 -0500271 }
Jason Monkfe8f6822015-12-21 15:12:01 -0500272 mService.onClick(mToken);
273 } catch (RemoteException e) {
274 // Called through wrapper, won't happen here.
Jason Monkbbadff82015-11-06 15:47:26 -0500275 }
Jason Monk5db8a412015-10-21 15:16:23 -0700276 MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
277 }
278
279 @Override
Jason Monk39c98e62016-03-16 09:18:35 -0400280 public CharSequence getTileLabel() {
281 return getState().label;
282 }
283
284 @Override
Jason Monk5db8a412015-10-21 15:16:23 -0700285 protected void handleUpdateState(State state, Object arg) {
Jason Monk1c2fea82016-03-11 11:33:36 -0500286 int tileState = mTile.getState();
287 if (mServiceManager.hasPendingBind()) {
288 tileState = Tile.STATE_UNAVAILABLE;
289 }
Jason Monk4a906f92016-04-20 10:54:55 -0400290 Drawable drawable;
Jason Monkd2274f82016-12-12 12:02:16 -0500291 boolean mHasRes = false;
292 android.graphics.drawable.Icon icon = mTile.getIcon();
Jason Monk4a906f92016-04-20 10:54:55 -0400293 try {
Jason Monkd2274f82016-12-12 12:02:16 -0500294 drawable = icon.loadDrawable(mAppContext);
295 mHasRes = icon.getType() == android.graphics.drawable.Icon.TYPE_RESOURCE;
Jason Monk4a906f92016-04-20 10:54:55 -0400296 } catch (Exception e) {
297 Log.w(TAG, "Invalid icon, forcing into unavailable state");
298 tileState = Tile.STATE_UNAVAILABLE;
Jason Monkd2274f82016-12-12 12:02:16 -0500299 drawable = mDefaultIcon.loadDrawable(mAppContext);
Jason Monk4a906f92016-04-20 10:54:55 -0400300 }
Jason Monk1c2fea82016-03-11 11:33:36 -0500301 int color = mContext.getColor(getColor(tileState));
Jason Monk94295132016-01-12 11:27:02 -0500302 drawable.setTint(color);
Jason Monkd2274f82016-12-12 12:02:16 -0500303 state.icon = mHasRes ? new DrawableIconWithRes(drawable, icon.getResId())
304 : new DrawableIcon(drawable);
Jason Monkbbadff82015-11-06 15:47:26 -0500305 state.label = mTile.getLabel();
Jason Monk1c2fea82016-03-11 11:33:36 -0500306 if (tileState == Tile.STATE_UNAVAILABLE) {
Jason Monk94295132016-01-12 11:27:02 -0500307 state.label = new SpannableStringBuilder().append(state.label,
308 new ForegroundColorSpan(color),
309 SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE);
310 }
Jason Monkbbadff82015-11-06 15:47:26 -0500311 if (mTile.getContentDescription() != null) {
312 state.contentDescription = mTile.getContentDescription();
313 } else {
Jason Monk5db8a412015-10-21 15:16:23 -0700314 state.contentDescription = state.label;
Jason Monk5db8a412015-10-21 15:16:23 -0700315 }
316 }
317
318 @Override
319 public int getMetricsCategory() {
Chris Wrenf6e9228b2016-01-26 18:04:35 -0500320 return MetricsEvent.QS_CUSTOM;
Jason Monk5db8a412015-10-21 15:16:23 -0700321 }
Jason Monkbbadff82015-11-06 15:47:26 -0500322
Jason Monk94295132016-01-12 11:27:02 -0500323 public void startUnlockAndRun() {
324 mHost.startRunnableDismissingKeyguard(new Runnable() {
325 @Override
326 public void run() {
327 try {
328 mService.onUnlockComplete();
329 } catch (RemoteException e) {
330 }
331 }
332 });
333 }
334
335 private static int getColor(int state) {
336 switch (state) {
337 case Tile.STATE_UNAVAILABLE:
338 return R.color.qs_tile_tint_unavailable;
339 case Tile.STATE_INACTIVE:
340 return R.color.qs_tile_tint_inactive;
341 case Tile.STATE_ACTIVE:
342 return R.color.qs_tile_tint_active;
343 }
344 return 0;
345 }
346
Jason Monk7e53f202016-01-28 10:40:20 -0500347 public static String toSpec(ComponentName name) {
348 return PREFIX + name.flattenToShortString() + ")";
349 }
350
Jason Monkbbadff82015-11-06 15:47:26 -0500351 public static ComponentName getComponentFromSpec(String spec) {
352 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
353 if (action.isEmpty()) {
354 throw new IllegalArgumentException("Empty custom tile spec action");
355 }
356 return ComponentName.unflattenFromString(action);
357 }
358
359 public static QSTile<?> create(QSTileHost host, String spec) {
360 if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
361 throw new IllegalArgumentException("Bad custom tile spec: " + spec);
362 }
363 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
364 if (action.isEmpty()) {
365 throw new IllegalArgumentException("Empty custom tile spec action");
366 }
367 return new CustomTile(host, action);
368 }
Jason Monk5db8a412015-10-21 15:16:23 -0700369}