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.Tile;
34import android.service.quicksettings.TileService;
35import android.util.ArrayMap;
36import android.util.Log;
37
38import com.android.internal.statusbar.StatusBarIcon;
39import com.android.systemui.Dependency;
40import com.android.systemui.qs.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(() -> mHost.getIconController().removeIcon(slot));
106        }
107    }
108
109    public void setMemoryPressure(boolean memoryPressure) {
110        mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND;
111        recalculateBindAllowance();
112    }
113
114    public void recalculateBindAllowance() {
115        final ArrayList<TileServiceManager> services;
116        synchronized (mServices) {
117            services = new ArrayList<>(mServices.values());
118        }
119        final int N = services.size();
120        if (N > mMaxBound) {
121            long currentTime = System.currentTimeMillis();
122            // Precalculate the priority of services for binding.
123            for (int i = 0; i < N; i++) {
124                services.get(i).calculateBindPriority(currentTime);
125            }
126            // Sort them so we can bind the most important first.
127            Collections.sort(services, SERVICE_SORT);
128        }
129        int i;
130        // Allow mMaxBound items to bind.
131        for (i = 0; i < mMaxBound && i < N; i++) {
132            services.get(i).setBindAllowed(true);
133        }
134        // The rest aren't allowed to bind for now.
135        while (i < N) {
136            services.get(i).setBindAllowed(false);
137            i++;
138        }
139    }
140
141    private void verifyCaller(CustomTile tile) {
142        try {
143            String packageName = tile.getComponent().getPackageName();
144            int uid = mContext.getPackageManager().getPackageUidAsUser(packageName,
145                    Binder.getCallingUserHandle().getIdentifier());
146            if (Binder.getCallingUid() != uid) {
147                throw new SecurityException("Component outside caller's uid");
148            }
149        } catch (PackageManager.NameNotFoundException e) {
150            throw new SecurityException(e);
151        }
152    }
153
154    private void requestListening(ComponentName component) {
155        synchronized (mServices) {
156            CustomTile customTile = getTileForComponent(component);
157            if (customTile == null) {
158                Log.d("TileServices", "Couldn't find tile for " + component);
159                return;
160            }
161            TileServiceManager service = mServices.get(customTile);
162            if (!service.isActiveTile()) {
163                return;
164            }
165            service.setBindRequested(true);
166            try {
167                service.getTileService().onStartListening();
168            } catch (RemoteException e) {
169            }
170        }
171    }
172
173    @Override
174    public void updateQsTile(Tile tile, IBinder token) {
175        CustomTile customTile = getTileForToken(token);
176        if (customTile != null) {
177            verifyCaller(customTile);
178            synchronized (mServices) {
179                final TileServiceManager tileServiceManager = mServices.get(customTile);
180                tileServiceManager.clearPendingBind();
181                tileServiceManager.setLastUpdate(System.currentTimeMillis());
182            }
183            customTile.updateState(tile);
184            customTile.refreshState();
185        }
186    }
187
188    @Override
189    public void onStartSuccessful(IBinder token) {
190        CustomTile customTile = getTileForToken(token);
191        if (customTile != null) {
192            verifyCaller(customTile);
193            synchronized (mServices) {
194                final TileServiceManager tileServiceManager = mServices.get(customTile);
195                tileServiceManager.clearPendingBind();
196            }
197            customTile.refreshState();
198        }
199    }
200
201    @Override
202    public void onShowDialog(IBinder token) {
203        CustomTile customTile = getTileForToken(token);
204        if (customTile != null) {
205            verifyCaller(customTile);
206            customTile.onDialogShown();
207            mHost.forceCollapsePanels();
208            mServices.get(customTile).setShowingDialog(true);
209        }
210    }
211
212    @Override
213    public void onDialogHidden(IBinder token) {
214        CustomTile customTile = getTileForToken(token);
215        if (customTile != null) {
216            verifyCaller(customTile);
217            mServices.get(customTile).setShowingDialog(false);
218            customTile.onDialogHidden();
219        }
220    }
221
222    @Override
223    public void onStartActivity(IBinder token) {
224        CustomTile customTile = getTileForToken(token);
225        if (customTile != null) {
226            verifyCaller(customTile);
227            mHost.forceCollapsePanels();
228        }
229    }
230
231    @Override
232    public void updateStatusIcon(IBinder token, Icon icon, String contentDescription) {
233        CustomTile customTile = getTileForToken(token);
234        if (customTile != null) {
235            verifyCaller(customTile);
236            try {
237                ComponentName componentName = customTile.getComponent();
238                String packageName = componentName.getPackageName();
239                UserHandle userHandle = getCallingUserHandle();
240                PackageInfo info = mContext.getPackageManager().getPackageInfoAsUser(packageName, 0,
241                        userHandle.getIdentifier());
242                if (info.applicationInfo.isSystemApp()) {
243                    final StatusBarIcon statusIcon = icon != null
244                            ? new StatusBarIcon(userHandle, packageName, icon, 0, 0,
245                                    contentDescription)
246                            : null;
247                    mMainHandler.post(new Runnable() {
248                        @Override
249                        public void run() {
250                            StatusBarIconController iconController = mHost.getIconController();
251                            iconController.setIcon(componentName.getClassName(), statusIcon);
252                            iconController.setExternalIcon(componentName.getClassName());
253                        }
254                    });
255                }
256            } catch (PackageManager.NameNotFoundException e) {
257            }
258        }
259    }
260
261    @Override
262    public Tile getTile(IBinder token) {
263        CustomTile customTile = getTileForToken(token);
264        if (customTile != null) {
265            verifyCaller(customTile);
266            return customTile.getQsTile();
267        }
268        return null;
269    }
270
271    @Override
272    public void startUnlockAndRun(IBinder token) {
273        CustomTile customTile = getTileForToken(token);
274        if (customTile != null) {
275            verifyCaller(customTile);
276            customTile.startUnlockAndRun();
277        }
278    }
279
280    @Override
281    public boolean isLocked() {
282        KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class);
283        return keyguardMonitor.isShowing();
284    }
285
286    @Override
287    public boolean isSecure() {
288        KeyguardMonitor keyguardMonitor = Dependency.get(KeyguardMonitor.class);
289        return keyguardMonitor.isSecure() && keyguardMonitor.isShowing();
290    }
291
292    private CustomTile getTileForToken(IBinder token) {
293        synchronized (mServices) {
294            return mTokenMap.get(token);
295        }
296    }
297
298    private CustomTile getTileForComponent(ComponentName component) {
299        synchronized (mServices) {
300            return mTiles.get(component);
301        }
302    }
303
304    public void destroy() {
305        synchronized (mServices) {
306            mServices.values().forEach(service -> service.handleDestroy());
307            mContext.unregisterReceiver(mRequestListeningReceiver);
308        }
309    }
310
311    private final BroadcastReceiver mRequestListeningReceiver = new BroadcastReceiver() {
312        @Override
313        public void onReceive(Context context, Intent intent) {
314            if (TileService.ACTION_REQUEST_LISTENING.equals(intent.getAction())) {
315                requestListening(
316                        (ComponentName) intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME));
317            }
318        }
319    };
320
321    private static final Comparator<TileServiceManager> SERVICE_SORT =
322            new Comparator<TileServiceManager>() {
323        @Override
324        public int compare(TileServiceManager left, TileServiceManager right) {
325            return -Integer.compare(left.getBindPriority(), right.getBindPriority());
326        }
327    };
328}
329