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