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.BroadcastReceiver;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.net.Uri;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.UserHandle;
30import android.service.quicksettings.IQSTileService;
31import android.service.quicksettings.Tile;
32import android.service.quicksettings.TileService;
33import android.support.annotation.VisibleForTesting;
34import android.util.Log;
35
36import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener;
37import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
38
39import java.util.List;
40
41import libcore.util.Objects;
42
43/**
44 * Manages the priority which lets {@link TileServices} make decisions about which tiles
45 * to bind.  Also holds on to and manages the {@link TileLifecycleManager}, informing it
46 * of when it is allowed to bind based on decisions frome the {@link TileServices}.
47 */
48public class TileServiceManager {
49
50    private static final long MIN_BIND_TIME = 5000;
51    private static final long UNBIND_DELAY = 30000;
52
53    public static final boolean DEBUG = true;
54
55    private static final String TAG = "TileServiceManager";
56
57    @VisibleForTesting
58    static final String PREFS_FILE = "CustomTileModes";
59
60    private final TileServices mServices;
61    private final TileLifecycleManager mStateManager;
62    private final Handler mHandler;
63    private boolean mBindRequested;
64    private boolean mBindAllowed;
65    private boolean mBound;
66    private int mPriority;
67    private boolean mJustBound;
68    private long mLastUpdate;
69    private boolean mShowingDialog;
70    // Whether we have a pending bind going out to the service without a response yet.
71    // This defaults to true to ensure tiles start out unavailable.
72    private boolean mPendingBind = true;
73
74    TileServiceManager(TileServices tileServices, Handler handler, ComponentName component,
75            Tile tile) {
76        this(tileServices, handler, new TileLifecycleManager(handler,
77                tileServices.getContext(), tileServices, tile, new Intent().setComponent(component),
78                new UserHandle(ActivityManager.getCurrentUser())));
79    }
80
81    @VisibleForTesting
82    TileServiceManager(TileServices tileServices, Handler handler,
83            TileLifecycleManager tileLifecycleManager) {
84        mServices = tileServices;
85        mHandler = handler;
86        mStateManager = tileLifecycleManager;
87
88        IntentFilter filter = new IntentFilter();
89        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
90        filter.addDataScheme("package");
91        Context context = mServices.getContext();
92        context.registerReceiverAsUser(mUninstallReceiver,
93                new UserHandle(ActivityManager.getCurrentUser()), filter, null, mHandler);
94        ComponentName component = tileLifecycleManager.getComponent();
95        if (!TileLifecycleManager.isTileAdded(context, component)) {
96            TileLifecycleManager.setTileAdded(context, component, true);
97            mStateManager.onTileAdded();
98            mStateManager.flushMessagesAndUnbind();
99        }
100    }
101
102    public void setTileChangeListener(TileChangeListener changeListener) {
103        mStateManager.setTileChangeListener(changeListener);
104    }
105
106    public boolean isActiveTile() {
107        return mStateManager.isActiveTile();
108    }
109
110    public void setShowingDialog(boolean dialog) {
111        mShowingDialog = dialog;
112    }
113
114    public IQSTileService getTileService() {
115        return mStateManager;
116    }
117
118    public IBinder getToken() {
119        return mStateManager.getToken();
120    }
121
122    public void setBindRequested(boolean bindRequested) {
123        if (mBindRequested == bindRequested) return;
124        mBindRequested = bindRequested;
125        if (mBindAllowed && mBindRequested && !mBound) {
126            mHandler.removeCallbacks(mUnbind);
127            bindService();
128        } else {
129            mServices.recalculateBindAllowance();
130        }
131        if (mBound && !mBindRequested) {
132            mHandler.postDelayed(mUnbind, UNBIND_DELAY);
133        }
134    }
135
136    public void setLastUpdate(long lastUpdate) {
137        mLastUpdate = lastUpdate;
138        if (mBound && isActiveTile()) {
139            mStateManager.onStopListening();
140            setBindRequested(false);
141        }
142        mServices.recalculateBindAllowance();
143    }
144
145    public void handleDestroy() {
146        mServices.getContext().unregisterReceiver(mUninstallReceiver);
147        mStateManager.handleDestroy();
148    }
149
150    public void setBindAllowed(boolean allowed) {
151        if (mBindAllowed == allowed) return;
152        mBindAllowed = allowed;
153        if (!mBindAllowed && mBound) {
154            unbindService();
155        } else if (mBindAllowed && mBindRequested && !mBound) {
156            bindService();
157        }
158    }
159
160    public boolean hasPendingBind() {
161        return mPendingBind;
162    }
163
164    public void clearPendingBind() {
165        mPendingBind = false;
166    }
167
168    private void bindService() {
169        if (mBound) {
170            Log.e(TAG, "Service already bound");
171            return;
172        }
173        mPendingBind = true;
174        mBound = true;
175        mJustBound = true;
176        mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
177        mStateManager.setBindService(true);
178    }
179
180    private void unbindService() {
181        if (!mBound) {
182            Log.e(TAG, "Service not bound");
183            return;
184        }
185        mBound = false;
186        mJustBound = false;
187        mStateManager.setBindService(false);
188    }
189
190    public void calculateBindPriority(long currentTime) {
191        if (mStateManager.hasPendingClick()) {
192            // Pending click is the most important thing, need to put this service at the top of
193            // the list to be bound.
194            mPriority = Integer.MAX_VALUE;
195        } else if (mShowingDialog) {
196            // Hang on to services that are showing dialogs so they don't die.
197            mPriority = Integer.MAX_VALUE - 1;
198        } else if (mJustBound) {
199            // If we just bound, lets not thrash on binding/unbinding too much, this is second most
200            // important.
201            mPriority = Integer.MAX_VALUE - 2;
202        } else if (!mBindRequested) {
203            // Don't care about binding right now, put us last.
204            mPriority = Integer.MIN_VALUE;
205        } else {
206            // Order based on whether this was just updated.
207            long timeSinceUpdate = currentTime - mLastUpdate;
208            // Fit compare into integer space for simplicity. Make sure to leave MAX_VALUE and
209            // MAX_VALUE - 1 for the more important states above.
210            if (timeSinceUpdate > Integer.MAX_VALUE - 3) {
211                mPriority = Integer.MAX_VALUE - 3;
212            } else {
213                mPriority = (int) timeSinceUpdate;
214            }
215        }
216    }
217
218    public int getBindPriority() {
219        return mPriority;
220    }
221
222    private final Runnable mUnbind = new Runnable() {
223        @Override
224        public void run() {
225            if (mBound && !mBindRequested) {
226                unbindService();
227            }
228        }
229    };
230
231    @VisibleForTesting
232    final Runnable mJustBoundOver = new Runnable() {
233        @Override
234        public void run() {
235            mJustBound = false;
236            mServices.recalculateBindAllowance();
237        }
238    };
239
240    private final BroadcastReceiver mUninstallReceiver = new BroadcastReceiver() {
241        @Override
242        public void onReceive(Context context, Intent intent) {
243            if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
244                return;
245            }
246
247            Uri data = intent.getData();
248            String pkgName = data.getEncodedSchemeSpecificPart();
249            final ComponentName component = mStateManager.getComponent();
250            if (!Objects.equal(pkgName, component.getPackageName())) {
251                return;
252            }
253
254            // If the package is being updated, verify the component still exists.
255            if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
256                Intent queryIntent = new Intent(TileService.ACTION_QS_TILE);
257                queryIntent.setPackage(pkgName);
258                PackageManager pm = context.getPackageManager();
259                List<ResolveInfo> services = pm.queryIntentServicesAsUser(
260                        queryIntent, 0, ActivityManager.getCurrentUser());
261                for (ResolveInfo info : services) {
262                    if (Objects.equal(info.serviceInfo.packageName, component.getPackageName())
263                            && Objects.equal(info.serviceInfo.name, component.getClassName())) {
264                        return;
265                    }
266                }
267            }
268
269            mServices.getHost().removeTile(component);
270        }
271    };
272}
273