blob: 411980b399bd11c37c6754d4341c1acc7a0f3bc2 [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
Gus Prevasab336792018-11-14 13:52:20 -050018import static android.view.Display.DEFAULT_DISPLAY;
19import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
20
Jason Monk724214a2016-02-19 16:43:00 -050021import android.app.ActivityManager;
Jason Monk5db8a412015-10-21 15:16:23 -070022import android.content.ComponentName;
Jason Monk76c67aa2016-02-19 14:49:42 -050023import android.content.Intent;
Jason Monk5db8a412015-10-21 15:16:23 -070024import android.content.pm.PackageManager;
Jason Monk724214a2016-02-19 16:43:00 -050025import android.content.pm.ResolveInfo;
Jason Monk5db8a412015-10-21 15:16:23 -070026import android.content.pm.ServiceInfo;
Jason Monk068cb8b2015-12-02 11:30:36 -050027import android.graphics.drawable.Drawable;
Jason Monk8c09ac72017-03-16 11:53:40 -040028import android.metrics.LogMaker;
Jason Monk76c67aa2016-02-19 14:49:42 -050029import android.net.Uri;
Jason Monk8f7f3182015-11-18 16:35:14 -050030import android.os.Binder;
Jason Monkbbadff82015-11-06 15:47:26 -050031import android.os.IBinder;
Jason Monk8f7f3182015-11-18 16:35:14 -050032import android.os.RemoteException;
Jason Monk76c67aa2016-02-19 14:49:42 -050033import android.provider.Settings;
Jason Monkbbadff82015-11-06 15:47:26 -050034import android.service.quicksettings.IQSTileService;
35import android.service.quicksettings.Tile;
Jason Monkfe8f6822015-12-21 15:12:01 -050036import android.service.quicksettings.TileService;
Amin Shaikhabcca632018-08-29 15:15:08 -040037import android.text.TextUtils;
Jason Monk1c6116c2017-09-06 17:33:01 -040038import android.text.format.DateUtils;
Jason Monkbbadff82015-11-06 15:47:26 -050039import android.util.Log;
Jason Monk8f7f3182015-11-18 16:35:14 -050040import android.view.IWindowManager;
Jason Monk8f7f3182015-11-18 16:35:14 -050041import android.view.WindowManagerGlobal;
Fabian Kozynski05843f02019-06-28 13:19:57 -040042import android.widget.Switch;
Amin Shaikhabcca632018-08-29 15:15:08 -040043
Tamas Berghammer383db5eb2016-06-22 15:21:38 +010044import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
Jason Monk9c7844c2017-01-18 15:21:53 -050045import com.android.systemui.Dependency;
Jason Monkec34da82017-02-24 15:57:05 -050046import com.android.systemui.plugins.ActivityStarter;
Jason Monk702e2eb2017-03-03 16:53:44 -050047import com.android.systemui.plugins.qs.QSTile.State;
Jason Monke5b770e2017-03-03 21:49:29 -050048import com.android.systemui.qs.QSTileHost;
Gus Prevasab336792018-11-14 13:52:20 -050049import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
50import com.android.systemui.qs.tileimpl.QSTileImpl;
Jason Monk5db8a412015-10-21 15:16:23 -070051
Gus Prevasab336792018-11-14 13:52:20 -050052import java.util.Objects;
Jason Monk32508852017-01-18 09:17:13 -050053
Jason Monk702e2eb2017-03-03 16:53:44 -050054public class CustomTile extends QSTileImpl<State> implements TileChangeListener {
Jason Monk5db8a412015-10-21 15:16:23 -070055 public static final String PREFIX = "custom(";
56
Jason Monk1c6116c2017-09-06 17:33:01 -040057 private static final long CUSTOM_STALE_TIMEOUT = DateUtils.HOUR_IN_MILLIS;
58
Jason Monk8f7f3182015-11-18 16:35:14 -050059 private static final boolean DEBUG = false;
60
Jason Monkbbadff82015-11-06 15:47:26 -050061 // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
62 // So instead we have a period of waiting.
63 private static final long UNBIND_DELAY = 30000;
Jason Monk5db8a412015-10-21 15:16:23 -070064
Jason Monkbbadff82015-11-06 15:47:26 -050065 private final ComponentName mComponent;
66 private final Tile mTile;
Jason Monk8f7f3182015-11-18 16:35:14 -050067 private final IWindowManager mWindowManager;
68 private final IBinder mToken = new Binder();
Jason Monkd5a204f2015-12-21 08:50:01 -050069 private final IQSTileService mService;
70 private final TileServiceManager mServiceManager;
Jason Monk1ffa11b2016-03-08 14:44:23 -050071 private final int mUser;
Jason Monk624cbe22016-05-02 10:42:17 -040072 private android.graphics.drawable.Icon mDefaultIcon;
Amin Shaikhabcca632018-08-29 15:15:08 -040073 private CharSequence mDefaultLabel;
Jason Monkbbadff82015-11-06 15:47:26 -050074
Jason Monkbbadff82015-11-06 15:47:26 -050075 private boolean mListening;
Jason Monk8f7f3182015-11-18 16:35:14 -050076 private boolean mIsTokenGranted;
77 private boolean mIsShowingDialog;
Jason Monkbbadff82015-11-06 15:47:26 -050078
79 private CustomTile(QSTileHost host, String action) {
Jason Monk5db8a412015-10-21 15:16:23 -070080 super(host);
Jason Monk8f7f3182015-11-18 16:35:14 -050081 mWindowManager = WindowManagerGlobal.getWindowManagerService();
Jason Monk5db8a412015-10-21 15:16:23 -070082 mComponent = ComponentName.unflattenFromString(action);
Jason Monkee68fd82016-06-23 13:12:23 -040083 mTile = new Tile();
Amin Shaikhabcca632018-08-29 15:15:08 -040084 updateDefaultTileAndIcon();
Jason Monkd5a204f2015-12-21 08:50:01 -050085 mServiceManager = host.getTileServices().getTileWrapper(this);
Fabian Kozynski05843f02019-06-28 13:19:57 -040086 if (mServiceManager.isBooleanTile()) {
87 // Replace states with BooleanState
88 resetStates();
89 }
90
Jason Monkd5a204f2015-12-21 08:50:01 -050091 mService = mServiceManager.getTileService();
Jason Monk624cbe22016-05-02 10:42:17 -040092 mServiceManager.setTileChangeListener(this);
Jason Monk1ffa11b2016-03-08 14:44:23 -050093 mUser = ActivityManager.getCurrentUser();
Jason Monk5db8a412015-10-21 15:16:23 -070094 }
95
Jason Monk1c6116c2017-09-06 17:33:01 -040096 @Override
97 protected long getStaleTimeout() {
98 return CUSTOM_STALE_TIMEOUT + DateUtils.MINUTE_IN_MILLIS * mHost.indexOf(getTileSpec());
99 }
100
Amin Shaikhabcca632018-08-29 15:15:08 -0400101 private void updateDefaultTileAndIcon() {
Jason Monk624cbe22016-05-02 10:42:17 -0400102 try {
103 PackageManager pm = mContext.getPackageManager();
Jeff Sharkeydd9bda82017-02-23 17:38:31 -0700104 int flags = PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE;
Will Harmon294af232016-06-24 17:02:34 -0700105 if (isSystemApp(pm)) {
106 flags |= PackageManager.MATCH_DISABLED_COMPONENTS;
107 }
Amin Shaikhabcca632018-08-29 15:15:08 -0400108
Will Harmon294af232016-06-24 17:02:34 -0700109 ServiceInfo info = pm.getServiceInfo(mComponent, flags);
Jason Monka5f6ed32016-06-22 09:58:46 -0400110 int icon = info.icon != 0 ? info.icon
111 : info.applicationInfo.icon;
Jason Monk624cbe22016-05-02 10:42:17 -0400112 // Update the icon if its not set or is the default icon.
113 boolean updateIcon = mTile.getIcon() == null
114 || iconEquals(mTile.getIcon(), mDefaultIcon);
Jason Monka5f6ed32016-06-22 09:58:46 -0400115 mDefaultIcon = icon != 0 ? android.graphics.drawable.Icon
116 .createWithResource(mComponent.getPackageName(), icon) : null;
Jason Monk624cbe22016-05-02 10:42:17 -0400117 if (updateIcon) {
118 mTile.setIcon(mDefaultIcon);
119 }
Amin Shaikhabcca632018-08-29 15:15:08 -0400120 // Update the label if there is no label or it is the default label.
121 boolean updateLabel = mTile.getLabel() == null
122 || TextUtils.equals(mTile.getLabel(), mDefaultLabel);
123 mDefaultLabel = info.loadLabel(pm);
124 if (updateLabel) {
125 mTile.setLabel(mDefaultLabel);
Jason Monk624cbe22016-05-02 10:42:17 -0400126 }
Amin Shaikhabcca632018-08-29 15:15:08 -0400127 } catch (PackageManager.NameNotFoundException e) {
Jason Monk624cbe22016-05-02 10:42:17 -0400128 mDefaultIcon = null;
Amin Shaikhabcca632018-08-29 15:15:08 -0400129 mDefaultLabel = null;
Jason Monk624cbe22016-05-02 10:42:17 -0400130 }
131 }
132
Will Harmon294af232016-06-24 17:02:34 -0700133 private boolean isSystemApp(PackageManager pm) throws PackageManager.NameNotFoundException {
134 return pm.getApplicationInfo(mComponent.getPackageName(), 0).isSystemApp();
135 }
136
Jason Monk624cbe22016-05-02 10:42:17 -0400137 /**
138 * Compare two icons, only works for resources.
139 */
140 private boolean iconEquals(android.graphics.drawable.Icon icon1,
141 android.graphics.drawable.Icon icon2) {
142 if (icon1 == icon2) {
143 return true;
144 }
145 if (icon1 == null || icon2 == null) {
146 return false;
147 }
148 if (icon1.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE
149 || icon2.getType() != android.graphics.drawable.Icon.TYPE_RESOURCE) {
150 return false;
151 }
152 if (icon1.getResId() != icon2.getResId()) {
153 return false;
154 }
Narayan Kamath607223f2018-02-19 14:09:02 +0000155 if (!Objects.equals(icon1.getResPackage(), icon2.getResPackage())) {
Jason Monk624cbe22016-05-02 10:42:17 -0400156 return false;
157 }
158 return true;
159 }
160
161 @Override
162 public void onTileChanged(ComponentName tile) {
Amin Shaikhabcca632018-08-29 15:15:08 -0400163 updateDefaultTileAndIcon();
Jason Monk624cbe22016-05-02 10:42:17 -0400164 }
165
Jason Monk1c2fea82016-03-11 11:33:36 -0500166 @Override
167 public boolean isAvailable() {
Jason Monk4a906f92016-04-20 10:54:55 -0400168 return mDefaultIcon != null;
Jason Monk1c2fea82016-03-11 11:33:36 -0500169 }
170
Jason Monk1ffa11b2016-03-08 14:44:23 -0500171 public int getUser() {
172 return mUser;
173 }
174
Jason Monkbbadff82015-11-06 15:47:26 -0500175 public ComponentName getComponent() {
176 return mComponent;
177 }
178
Jason Monk8c09ac72017-03-16 11:53:40 -0400179 @Override
Jason Monkcb4b31d2017-05-03 10:37:34 -0400180 public LogMaker populate(LogMaker logMaker) {
Jason Monk8c09ac72017-03-16 11:53:40 -0400181 return super.populate(logMaker).setComponentName(mComponent);
182 }
183
Jason Monkbbadff82015-11-06 15:47:26 -0500184 public Tile getQsTile() {
Amin Shaikhabcca632018-08-29 15:15:08 -0400185 updateDefaultTileAndIcon();
Jason Monkbbadff82015-11-06 15:47:26 -0500186 return mTile;
187 }
188
189 public void updateState(Tile tile) {
Jason Monkbbadff82015-11-06 15:47:26 -0500190 mTile.setIcon(tile.getIcon());
191 mTile.setLabel(tile.getLabel());
Evan Laird66a8e732018-12-12 13:52:28 -0500192 mTile.setSubtitle(tile.getSubtitle());
Jason Monkbbadff82015-11-06 15:47:26 -0500193 mTile.setContentDescription(tile.getContentDescription());
Jason Monk94295132016-01-12 11:27:02 -0500194 mTile.setState(tile.getState());
Jason Monk5db8a412015-10-21 15:16:23 -0700195 }
196
Jason Monk8f7f3182015-11-18 16:35:14 -0500197 public void onDialogShown() {
198 mIsShowingDialog = true;
199 }
200
Jason Monk34a5cef2016-01-29 11:28:44 -0500201 public void onDialogHidden() {
202 mIsShowingDialog = false;
203 try {
204 if (DEBUG) Log.d(TAG, "Removing token");
Wale Ogunwaleac2561e2016-11-01 15:43:46 -0700205 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
Jason Monk34a5cef2016-01-29 11:28:44 -0500206 } catch (RemoteException e) {
207 }
208 }
209
Jason Monk5db8a412015-10-21 15:16:23 -0700210 @Override
Jason Monk1c6116c2017-09-06 17:33:01 -0400211 public void handleSetListening(boolean listening) {
Jason Monkbbadff82015-11-06 15:47:26 -0500212 if (mListening == listening) return;
213 mListening = listening;
Jason Monkd5a204f2015-12-21 08:50:01 -0500214 try {
215 if (listening) {
Amin Shaikhabcca632018-08-29 15:15:08 -0400216 updateDefaultTileAndIcon();
Jason Monk624cbe22016-05-02 10:42:17 -0400217 refreshState();
Jason Monk97d22722016-04-07 11:41:47 -0400218 if (!mServiceManager.isActiveTile()) {
Jason Monkfe8f6822015-12-21 15:12:01 -0500219 mServiceManager.setBindRequested(true);
220 mService.onStartListening();
221 }
Jason Monkdc35dcb2015-12-04 16:36:15 -0500222 } else {
Jason Monkbbadff82015-11-06 15:47:26 -0500223 mService.onStopListening();
Jason Monkd5a204f2015-12-21 08:50:01 -0500224 if (mIsTokenGranted && !mIsShowingDialog) {
225 try {
226 if (DEBUG) Log.d(TAG, "Removing token");
Wale Ogunwaleac2561e2016-11-01 15:43:46 -0700227 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
Jason Monkd5a204f2015-12-21 08:50:01 -0500228 } catch (RemoteException e) {
229 }
230 mIsTokenGranted = false;
Jason Monk8f7f3182015-11-18 16:35:14 -0500231 }
Jason Monkd5a204f2015-12-21 08:50:01 -0500232 mIsShowingDialog = false;
233 mServiceManager.setBindRequested(false);
Jason Monk8f7f3182015-11-18 16:35:14 -0500234 }
Jason Monkd5a204f2015-12-21 08:50:01 -0500235 } catch (RemoteException e) {
236 // Called through wrapper, won't happen here.
Jason Monkbbadff82015-11-06 15:47:26 -0500237 }
238 }
Jason Monk8f7f3182015-11-18 16:35:14 -0500239
Jason Monkbbadff82015-11-06 15:47:26 -0500240 @Override
241 protected void handleDestroy() {
242 super.handleDestroy();
Jason Monk8f7f3182015-11-18 16:35:14 -0500243 if (mIsTokenGranted) {
244 try {
245 if (DEBUG) Log.d(TAG, "Removing token");
Wale Ogunwaleac2561e2016-11-01 15:43:46 -0700246 mWindowManager.removeWindowToken(mToken, DEFAULT_DISPLAY);
Jason Monk8f7f3182015-11-18 16:35:14 -0500247 } catch (RemoteException e) {
248 }
249 }
Jason Monk66c89c12016-01-06 08:51:26 -0500250 mHost.getTileServices().freeService(this, mServiceManager);
Jason Monk5db8a412015-10-21 15:16:23 -0700251 }
252
253 @Override
Jason Monk62b63a02016-02-02 15:15:31 -0500254 public State newTileState() {
Fabian Kozynski05843f02019-06-28 13:19:57 -0400255 if (mServiceManager != null && mServiceManager.isBooleanTile()) {
256 return new BooleanState();
257 }
258 return new State();
Jason Monk5db8a412015-10-21 15:16:23 -0700259 }
260
261 @Override
Jason Monk76c67aa2016-02-19 14:49:42 -0500262 public Intent getLongClickIntent() {
Jason Monk724214a2016-02-19 16:43:00 -0500263 Intent i = new Intent(TileService.ACTION_QS_TILE_PREFERENCES);
264 i.setPackage(mComponent.getPackageName());
265 i = resolveIntent(i);
266 if (i != null) {
Jeff Sharkey1a749422017-04-28 14:19:50 -0600267 i.putExtra(Intent.EXTRA_COMPONENT_NAME, mComponent);
Akira Oshimi8d2e9a92017-01-24 16:50:53 +0900268 i.putExtra(TileService.EXTRA_STATE, mTile.getState());
Jason Monk724214a2016-02-19 16:43:00 -0500269 return i;
270 }
Jason Monk76c67aa2016-02-19 14:49:42 -0500271 return new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(
272 Uri.fromParts("package", mComponent.getPackageName(), null));
Jason Monk5db8a412015-10-21 15:16:23 -0700273 }
274
Jason Monk724214a2016-02-19 16:43:00 -0500275 private Intent resolveIntent(Intent i) {
276 ResolveInfo result = mContext.getPackageManager().resolveActivityAsUser(i, 0,
277 ActivityManager.getCurrentUser());
278 return result != null ? new Intent(TileService.ACTION_QS_TILE_PREFERENCES)
279 .setClassName(result.activityInfo.packageName, result.activityInfo.name) : null;
280 }
281
Jason Monk5db8a412015-10-21 15:16:23 -0700282 @Override
283 protected void handleClick() {
Jason Monk94295132016-01-12 11:27:02 -0500284 if (mTile.getState() == Tile.STATE_UNAVAILABLE) {
285 return;
286 }
Jason Monkfe8f6822015-12-21 15:12:01 -0500287 try {
288 if (DEBUG) Log.d(TAG, "Adding token");
Wale Ogunwaleac2561e2016-11-01 15:43:46 -0700289 mWindowManager.addWindowToken(mToken, TYPE_QS_DIALOG, DEFAULT_DISPLAY);
Jason Monkfe8f6822015-12-21 15:12:01 -0500290 mIsTokenGranted = true;
291 } catch (RemoteException e) {
292 }
293 try {
Jason Monk97d22722016-04-07 11:41:47 -0400294 if (mServiceManager.isActiveTile()) {
Jason Monkfe8f6822015-12-21 15:12:01 -0500295 mServiceManager.setBindRequested(true);
296 mService.onStartListening();
Jason Monk8f7f3182015-11-18 16:35:14 -0500297 }
Jason Monkfe8f6822015-12-21 15:12:01 -0500298 mService.onClick(mToken);
299 } catch (RemoteException e) {
300 // Called through wrapper, won't happen here.
Jason Monkbbadff82015-11-06 15:47:26 -0500301 }
Jason Monk5db8a412015-10-21 15:16:23 -0700302 }
303
304 @Override
Jason Monk39c98e62016-03-16 09:18:35 -0400305 public CharSequence getTileLabel() {
306 return getState().label;
307 }
308
309 @Override
Jason Monk5db8a412015-10-21 15:16:23 -0700310 protected void handleUpdateState(State state, Object arg) {
Jason Monk1c2fea82016-03-11 11:33:36 -0500311 int tileState = mTile.getState();
312 if (mServiceManager.hasPendingBind()) {
313 tileState = Tile.STATE_UNAVAILABLE;
314 }
Jason Monk32508852017-01-18 09:17:13 -0500315 state.state = tileState;
Jason Monk4a906f92016-04-20 10:54:55 -0400316 Drawable drawable;
317 try {
Jason Monk32508852017-01-18 09:17:13 -0500318 drawable = mTile.getIcon().loadDrawable(mContext);
Jason Monk4a906f92016-04-20 10:54:55 -0400319 } catch (Exception e) {
320 Log.w(TAG, "Invalid icon, forcing into unavailable state");
Jason Monk6f352aa2017-03-03 09:10:50 -0500321 state.state = Tile.STATE_UNAVAILABLE;
Jason Monk32508852017-01-18 09:17:13 -0500322 drawable = mDefaultIcon.loadDrawable(mContext);
Jason Monk4a906f92016-04-20 10:54:55 -0400323 }
Evan Lairdb3daf2b2017-10-17 16:00:29 -0400324
325 final Drawable drawableF = drawable;
326 state.iconSupplier = () -> {
327 Drawable.ConstantState cs = drawableF.getConstantState();
328 if (cs != null) {
329 return new DrawableIcon(cs.newDrawable());
330 }
331 return null;
332 };
Jason Monkbbadff82015-11-06 15:47:26 -0500333 state.label = mTile.getLabel();
Evan Laird66a8e732018-12-12 13:52:28 -0500334
335 CharSequence subtitle = mTile.getSubtitle();
336 if (subtitle != null && subtitle.length() > 0) {
337 state.secondaryLabel = subtitle;
338 } else {
339 state.secondaryLabel = null;
340 }
341
Jason Monkbbadff82015-11-06 15:47:26 -0500342 if (mTile.getContentDescription() != null) {
343 state.contentDescription = mTile.getContentDescription();
344 } else {
Jason Monk5db8a412015-10-21 15:16:23 -0700345 state.contentDescription = state.label;
Jason Monk5db8a412015-10-21 15:16:23 -0700346 }
Fabian Kozynski05843f02019-06-28 13:19:57 -0400347
348 if (state instanceof BooleanState) {
349 state.expandedAccessibilityClassName = Switch.class.getName();
350 ((BooleanState) state).value = (state.state == Tile.STATE_ACTIVE);
351 }
352
Jason Monk5db8a412015-10-21 15:16:23 -0700353 }
354
355 @Override
356 public int getMetricsCategory() {
Chris Wrenf6e9228b2016-01-26 18:04:35 -0500357 return MetricsEvent.QS_CUSTOM;
Jason Monk5db8a412015-10-21 15:16:23 -0700358 }
Jason Monkbbadff82015-11-06 15:47:26 -0500359
Jason Monk94295132016-01-12 11:27:02 -0500360 public void startUnlockAndRun() {
Jason Monk9c7844c2017-01-18 15:21:53 -0500361 Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() -> {
362 try {
363 mService.onUnlockComplete();
364 } catch (RemoteException e) {
Jason Monk94295132016-01-12 11:27:02 -0500365 }
366 });
367 }
368
Jason Monk7e53f202016-01-28 10:40:20 -0500369 public static String toSpec(ComponentName name) {
370 return PREFIX + name.flattenToShortString() + ")";
371 }
372
Jason Monkbbadff82015-11-06 15:47:26 -0500373 public static ComponentName getComponentFromSpec(String spec) {
374 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
375 if (action.isEmpty()) {
376 throw new IllegalArgumentException("Empty custom tile spec action");
377 }
378 return ComponentName.unflattenFromString(action);
379 }
380
Amin Shaikh8c4a19c2018-03-20 09:09:17 -0400381 public static CustomTile create(QSTileHost host, String spec) {
Jason Monkbbadff82015-11-06 15:47:26 -0500382 if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
383 throw new IllegalArgumentException("Bad custom tile spec: " + spec);
384 }
385 final String action = spec.substring(PREFIX.length(), spec.length() - 1);
386 if (action.isEmpty()) {
387 throw new IllegalArgumentException("Empty custom tile spec action");
388 }
389 return new CustomTile(host, action);
390 }
Jason Monk5db8a412015-10-21 15:16:23 -0700391}