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