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.content.BroadcastReceiver;
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.content.IntentFilter;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.graphics.drawable.Icon;
26import android.os.Binder;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Looper;
30import android.os.RemoteException;
31import android.os.UserHandle;
32import android.service.quicksettings.IQSService;
33import android.service.quicksettings.IQSTileService;
34import android.service.quicksettings.Tile;
35import android.service.quicksettings.TileService;
36import android.util.ArrayMap;
37import android.util.Log;
38
39import com.android.internal.statusbar.StatusBarIcon;
40import com.android.systemui.statusbar.phone.QSTileHost;
41import com.android.systemui.statusbar.phone.StatusBarIconController;
42import com.android.systemui.statusbar.policy.KeyguardMonitor;
43
44import java.util.ArrayList;
45import java.util.Collections;
46import java.util.Comparator;
47
48/**
49 * Runs the day-to-day operations of which tiles should be bound and when.
50 */
51public class TileServices extends IQSService.Stub {
52    static final int DEFAULT_MAX_BOUND = 3;
53    static final int REDUCED_MAX_BOUND = 1;
54
55    private final ArrayMap<CustomTile, TileServiceManager> mServices = new ArrayMap<>();
56    private final ArrayMap<ComponentName, CustomTile> mTiles = new ArrayMap<>();
57    private final ArrayMap<IBinder, CustomTile> mTokenMap = new ArrayMap<>();
58    private final Context mContext;
59    private final Handler mHandler;
60    private final Handler mMainHandler;
61    private final QSTileHost mHost;
62
63    private int mMaxBound = DEFAULT_MAX_BOUND;
64
65    public TileServices(QSTileHost host, Looper looper) {
66        mHost = host;
67        mContext = mHost.getContext();
68        mContext.registerReceiver(mRequestListeningReceiver,
69                new IntentFilter(TileService.ACTION_REQUEST_LISTENING));
70        mHandler = new Handler(looper);
71        mMainHandler = new Handler(Looper.getMainLooper());
72    }
73
74    public Context getContext() {
75        return mContext;
76    }
77
78    public QSTileHost getHost() {
79        return mHost;
80    }
81
82    public TileServiceManager getTileWrapper(CustomTile tile) {
83        ComponentName component = tile.getComponent();
84        TileServiceManager service = onCreateTileService(component, tile.getQsTile());
85        synchronized (mServices) {
86            mServices.put(tile, service);
87            mTiles.put(component, tile);
88            mTokenMap.put(service.getToken(), tile);
89        }
90        return service;
91    }
92
93    protected TileServiceManager onCreateTileService(ComponentName component, Tile tile) {
94        return new TileServiceManager(this, mHandler, component, tile);
95    }
96
97    public void freeService(CustomTile tile, TileServiceManager service) {
98        synchronized (mServices) {
99            service.setBindAllowed(false);
100            service.handleDestroy();
101            mServices.remove(tile);
102            mTokenMap.remove(service.getToken());
103            mTiles.remove(tile.getComponent());
104            final String slot = tile.getComponent().getClassName();
105            mMainHandler.post(new Runnable() {
106                @Override
107                public void run() {
108                    mHost.getIconController().removeIcon(slot);
109                }
110            });
111        }
112    }
113
114    public void setMemoryPressure(boolean memoryPressure) {
115        mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND;
116        recalculateBindAllowance();
117    }
118
119    public void recalculateBindAllowance() {
120        final ArrayList<TileServiceManager> services;
121        synchronized (mServices) {
122            services = new ArrayList<>(mServices.values());
123        }
124        final int N = services.size();
125        if (N > mMaxBound) {
126            long currentTime = System.currentTimeMillis();
127            // Precalculate the priority of services for binding.
128            for (int i = 0; i < N; i++) {
129                services.get(i).calculateBindPriority(currentTime);
130            }
131            // Sort them so we can bind the most important first.
132            Collections.sort(services, SERVICE_SORT);
133        }
134        int i;
135        // Allow mMaxBound items to bind.
136        for (i = 0; i < mMaxBound && i < N; i++) {
137            services.get(i).setBindAllowed(true);
138        }
139        // The rest aren't allowed to bind for now.
140        while (i < N) {
141            services.get(i).setBindAllowed(false);
142            i++;
143        }
144    }
145
146    private void verifyCaller(CustomTile tile) {
147        try {
148            String packageName = tile.getComponent().getPackageName();
149            int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
150                    Binder.getCallingUserHandle().getIdentifier());
151            if (Binder.getCallingUid() != uid) {
152                throw new SecurityException("Component outside caller's uid");
153            }
154        } catch (PackageManager.NameNotFoundException e) {
155            throw new SecurityException(e);
156        }
157    }
158
159    private void requestListening(ComponentName component) {
160        synchronized (mServices) {
161            CustomTile customTile = getTileForComponent(component);
162            if (customTile == null) {
163                Log.d("TileServices", "Couldn't find tile for " + component);
164                return;
165            }
166            TileServiceManager service = mServices.get(customTile);
167            if (!service.isActiveTile()) {
168                return;
169            }
170            service.setBindRequested(true);
171            try {
172                service.getTileService().onStartListening();
173            } catch (RemoteException e) {
174            }
175        }
176    }
177
178    @Override
179    public void updateQsTile(Tile tile, IBinder token) {
180        CustomTile customTile = getTileForToken(token);
181        if (customTile != null) {
182            verifyCaller(customTile);
183            synchronized (mServices) {
184                final TileServiceManager tileServiceManager = mServices.get(customTile);
185                tileServiceManager.clearPendingBind();
186                tileServiceManager.setLastUpdate(System.currentTimeMillis());
187            }
188            customTile.updateState(tile);
189            customTile.refreshState();
190        }
191    }
192
193    @Override
194    public void onStartSuccessful(IBinder token) {
195        CustomTile customTile = getTileForToken(token);
196        if (customTile != null) {
197            verifyCaller(customTile);
198            synchronized (mServices) {
199                final TileServiceManager tileServiceManager = mServices.get(customTile);
200                tileServiceManager.clearPendingBind();
201            }
202            customTile.refreshState();
203        }
204    }
205
206    @Override
207    public void onShowDialog(IBinder token) {
208        CustomTile customTile = getTileForToken(token);
209        if (customTile != null) {
210            verifyCaller(customTile);
211            customTile.onDialogShown();
212            mHost.collapsePanels();
213            mServices.get(customTile).setShowingDialog(true);
214        }
215    }
216
217    @Override
218    public void onDialogHidden(IBinder token) {
219        CustomTile customTile = getTileForToken(token);
220        if (customTile != null) {
221            verifyCaller(customTile);
222            mServices.get(customTile).setShowingDialog(false);
223            customTile.onDialogHidden();
224        }
225    }
226
227    @Override
228    public void onStartActivity(IBinder token) {
229        CustomTile customTile = getTileForToken(token);
230        if (customTile != null) {
231            verifyCaller(customTile);
232            mHost.collapsePanels();
233        }
234    }
235
236    @Override
237    public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
238        CustomTile customTile = getTileForToken(token);
239        if (customTile != null) {
240            verifyCaller(customTile);
241            try {
242                ComponentName componentName = customTile.getComponent();
243                String packageName = componentName.getPackageName();
244                UserHandle userHandle = getCallingUserHandle();
245                PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0,
246                        userHandle.getIdentifier());
247                if (info.applicationInfo.isSystemApp()) {
248                    final StatusBarIcon statusIcon = icon != null
249                            ? new StatusBarIcon(userHandle, packageName, icon, 0, 0,
250                                    contentDescription)
251                            : null;
252                    mMainHandler.post(new Runnable() {
253                        @Override
254                        public void run() {
255                            StatusBarIconController iconController = mHost.getIconController();
256                            iconController.setIcon(componentName.getClassName(), statusIcon);
257                            iconController.setExternalIcon(componentName.getClassName());
258                        }
259                    });
260                }
261            } catch (PackageManager.NameNotFoundException e) {
262            }
263        }
264    }
265
266    @Override
267    public Tile getTile(IBinder token) {
268        CustomTile customTile = getTileForToken(token);
269        if (customTile != null) {
270            verifyCaller(customTile);
271            return customTile.getQsTile();
272        }
273        return null;
274    }
275
276    @Override
277    public void startUnlockAndRun(IBinder token) {
278        CustomTile customTile = getTileForToken(token);
279        if (customTile != null) {
280            verifyCaller(customTile);
281            customTile.startUnlockAndRun();
282        }
283    }
284
285    @Override
286    public boolean isLocked() {
287        KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor();
288        return keyguardMonitor.isShowing();
289    }
290
291    @Override
292    public boolean isSecure() {
293        KeyguardMonitor keyguardMonitor = mHost.getKeyguardMonitor();
294        return keyguardMonitor.isSecure() && keyguardMonitor.isShowing();
295    }
296
297    private CustomTile getTileForToken(IBinder token) {
298        synchronized (mServices) {
299            return mTokenMap.get(token);
300        }
301    }
302
303    private CustomTile getTileForComponent(ComponentName component) {
304        synchronized (mServices) {
305            return mTiles.get(component);
306        }
307    }
308
309    private final BroadcastReceiver mRequestListeningReceiver = new BroadcastReceiver() {
310        @Override
311        public void onReceive(Context context, Intent intent) {
312            if (TileService.ACTION_REQUEST_LISTENING.equals(intent.getAction())) {
313                requestListening(
314                        (ComponentName) intent.getParcelableExtra(TileService.EXTRA_COMPONENT));
315            }
316        }
317    };
318
319    private static final Comparator<TileServiceManager> SERVICE_SORT =
320            new Comparator<TileServiceManager>() {
321        @Override
322        public int compare(TileServiceManager left, TileServiceManager right) {
323            return -Integer.compare(left.getBindPriority(), right.getBindPriority());
324        }
325    };
326}
327