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