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