1/*
2 * Copyright (C) 2016 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 */
16
17package com.android.systemui.qs.customize;
18
19import android.Manifest.permission;
20import android.app.ActivityManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.graphics.drawable.Drawable;
27import android.os.Handler;
28import android.os.Looper;
29import android.service.quicksettings.TileService;
30import android.widget.Button;
31
32import com.android.systemui.Dependency;
33import com.android.systemui.R;
34import com.android.systemui.plugins.qs.QSTile;
35import com.android.systemui.plugins.qs.QSTile.State;
36import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
37import com.android.systemui.qs.external.CustomTile;
38import com.android.systemui.qs.QSTileHost;
39
40import java.util.ArrayList;
41import java.util.Collection;
42import java.util.List;
43
44public class TileQueryHelper {
45
46    private static final String TAG = "TileQueryHelper";
47
48    private final ArrayList<TileInfo> mTiles = new ArrayList<>();
49    private final ArrayList<String> mSpecs = new ArrayList<>();
50    private final Context mContext;
51    private final TileStateListener mListener;
52    private final QSTileHost mHost;
53    private final Runnable mCompletion;
54
55    public TileQueryHelper(Context context, QSTileHost host,
56            TileStateListener listener, Runnable completion) {
57        mContext = context;
58        mListener = listener;
59        mHost = host;
60        mCompletion = completion;
61        addSystemTiles();
62        // TODO: Live?
63    }
64
65    private void addSystemTiles() {
66        // Enqueue jobs to fetch every system tile and then ever package tile.
67        final Handler qsHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
68        final Handler mainHandler = new Handler(Looper.getMainLooper());
69        addStockTiles(mainHandler, qsHandler);
70        addPackageTiles(mainHandler, qsHandler);
71        // Then enqueue the completion. It should always be last
72        qsHandler.post(mCompletion);
73    }
74
75    private void addStockTiles(Handler mainHandler, Handler bgHandler) {
76        String possible = mContext.getString(R.string.quick_settings_tiles_stock);
77        String[] possibleTiles = possible.split(",");
78        for (int i = 0; i < possibleTiles.length; i++) {
79            final String spec = possibleTiles[i];
80            final QSTile tile = mHost.createTile(spec);
81            if (tile == null) {
82                continue;
83            } else if (!tile.isAvailable()) {
84                tile.destroy();
85                continue;
86            }
87            tile.setListening(this, true);
88            tile.clearState();
89            tile.refreshState();
90            tile.setListening(this, false);
91            bgHandler.post(new Runnable() {
92                @Override
93                public void run() {
94                    final QSTile.State state = tile.getState().copy();
95                    // Ignore the current state and get the generic label instead.
96                    state.label = tile.getTileLabel();
97                    tile.destroy();
98                    mainHandler.post(new Runnable() {
99                        @Override
100                        public void run() {
101                            addTile(spec, null, state, true);
102                            mListener.onTilesChanged(mTiles);
103                        }
104                    });
105                }
106            });
107        }
108    }
109
110    private void addPackageTiles(Handler mainHandler, Handler bgHandler) {
111        bgHandler.post(() -> {
112            Collection<QSTile> params = mHost.getTiles();
113            PackageManager pm = mContext.getPackageManager();
114            List<ResolveInfo> services = pm.queryIntentServicesAsUser(
115                    new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
116            String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
117
118            for (ResolveInfo info : services) {
119                String packageName = info.serviceInfo.packageName;
120                ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
121
122                // Don't include apps that are a part of the default tile set.
123                if (stockTiles.contains(componentName.flattenToString())) {
124                    continue;
125                }
126
127                final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
128                String spec = CustomTile.toSpec(componentName);
129                State state = getState(params, spec);
130                if (state != null) {
131                    addTile(spec, appLabel, state, false);
132                    continue;
133                }
134                if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) {
135                    continue;
136                }
137                Drawable icon = info.serviceInfo.loadIcon(pm);
138                if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) {
139                    continue;
140                }
141                if (icon == null) {
142                    continue;
143                }
144                icon.mutate();
145                icon.setTint(mContext.getColor(android.R.color.white));
146                CharSequence label = info.serviceInfo.loadLabel(pm);
147                addTile(spec, icon, label != null ? label.toString() : "null", appLabel, mContext);
148            }
149            mainHandler.post(() -> mListener.onTilesChanged(mTiles));
150        });
151    }
152
153    private State getState(Collection<QSTile> tiles, String spec) {
154        for (QSTile tile : tiles) {
155            if (spec.equals(tile.getTileSpec())) {
156                return tile.getState().copy();
157            }
158        }
159        return null;
160    }
161
162    private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) {
163        if (mSpecs.contains(spec)) {
164            return;
165        }
166        TileInfo info = new TileInfo();
167        info.state = state;
168        info.state.dualTarget = false; // No dual targets in edit.
169        info.state.expandedAccessibilityClassName =
170                Button.class.getName();
171        info.spec = spec;
172        info.appLabel = appLabel;
173        info.isSystem = isSystem;
174        mTiles.add(info);
175        mSpecs.add(spec);
176    }
177
178    private void addTile(String spec, Drawable drawable, CharSequence label, CharSequence appLabel,
179            Context context) {
180        QSTile.State state = new QSTile.State();
181        state.label = label;
182        state.contentDescription = label;
183        state.icon = new DrawableIcon(drawable);
184        addTile(spec, appLabel, state, false);
185    }
186
187    public static class TileInfo {
188        public String spec;
189        public CharSequence appLabel;
190        public QSTile.State state;
191        public boolean isSystem;
192    }
193
194    public interface TileStateListener {
195        void onTilesChanged(List<TileInfo> tiles);
196    }
197}
198