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  */
16 package com.android.systemui.qs.external;
17 
18 import android.content.BroadcastReceiver;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.IntentFilter;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.net.Uri;
26 import android.os.Handler;
27 import android.os.IBinder;
28 import android.service.quicksettings.IQSTileService;
29 import android.service.quicksettings.TileService;
30 import android.util.Log;
31 
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.systemui.broadcast.BroadcastDispatcher;
35 import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
36 import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository;
37 import com.android.systemui.settings.UserTracker;
38 import com.android.systemui.util.concurrency.DelayableExecutor;
39 
40 import java.util.List;
41 import java.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  */
48 public 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 final UserTracker mUserTracker;
64     private final CustomTileAddedRepository mCustomTileAddedRepository;
65     private boolean mBindRequested;
66     private boolean mBindAllowed;
67     private boolean mBound;
68     private int mPriority;
69     private boolean mJustBound;
70     private long mLastUpdate;
71     private boolean mShowingDialog;
72     // Whether we have a pending bind going out to the service without a response yet.
73     // This defaults to true to ensure tiles start out unavailable.
74     private boolean mPendingBind = true;
75     private boolean mStarted = false;
76 
TileServiceManager(TileServices tileServices, Handler handler, ComponentName component, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker, CustomTileAddedRepository customTileAddedRepository, DelayableExecutor executor)77     TileServiceManager(TileServices tileServices, Handler handler, ComponentName component,
78             BroadcastDispatcher broadcastDispatcher, UserTracker userTracker,
79             CustomTileAddedRepository customTileAddedRepository, DelayableExecutor executor) {
80         this(tileServices, handler, userTracker, customTileAddedRepository,
81                 new TileLifecycleManager(handler, tileServices.getContext(), tileServices,
82                         new PackageManagerAdapter(tileServices.getContext()), broadcastDispatcher,
83                         new Intent(TileService.ACTION_QS_TILE).setComponent(component),
84                         userTracker.getUserHandle(), executor));
85     }
86 
87     @VisibleForTesting
TileServiceManager(TileServices tileServices, Handler handler, UserTracker userTracker, CustomTileAddedRepository customTileAddedRepository, TileLifecycleManager tileLifecycleManager)88     TileServiceManager(TileServices tileServices, Handler handler, UserTracker userTracker,
89             CustomTileAddedRepository customTileAddedRepository,
90             TileLifecycleManager tileLifecycleManager) {
91         mServices = tileServices;
92         mHandler = handler;
93         mStateManager = tileLifecycleManager;
94         mUserTracker = userTracker;
95         mCustomTileAddedRepository = customTileAddedRepository;
96 
97         IntentFilter filter = new IntentFilter();
98         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
99         filter.addDataScheme("package");
100         Context context = mServices.getContext();
101         context.registerReceiverAsUser(mUninstallReceiver, userTracker.getUserHandle(), filter,
102                 null, mHandler, Context.RECEIVER_EXPORTED);
103     }
104 
isLifecycleStarted()105     boolean isLifecycleStarted() {
106         return mStarted;
107     }
108 
109     /**
110      * Starts the TileLifecycleManager by adding the corresponding component as a Tile and
111      * binding to it if needed.
112      *
113      * This method should be called after constructing a TileServiceManager to guarantee that the
114      * TileLifecycleManager has added the tile and bound to it at least once.
115      */
startLifecycleManagerAndAddTile()116     void startLifecycleManagerAndAddTile() {
117         mStarted = true;
118         ComponentName component = mStateManager.getComponent();
119         final int userId = mStateManager.getUserId();
120         if (!mCustomTileAddedRepository.isTileAdded(component, userId)) {
121             mCustomTileAddedRepository.setTileAdded(component, userId, true);
122             mStateManager.onTileAdded();
123             mStateManager.flushMessagesAndUnbind();
124         }
125     }
126 
setTileChangeListener(TileChangeListener changeListener)127     public void setTileChangeListener(TileChangeListener changeListener) {
128         mStateManager.setTileChangeListener(changeListener);
129     }
130 
isActiveTile()131     public boolean isActiveTile() {
132         return mStateManager.isActiveTile();
133     }
134 
isToggleableTile()135     public boolean isToggleableTile() {
136         return mStateManager.isToggleableTile();
137     }
138 
setShowingDialog(boolean dialog)139     public void setShowingDialog(boolean dialog) {
140         mShowingDialog = dialog;
141     }
142 
getTileService()143     public IQSTileService getTileService() {
144         return mStateManager;
145     }
146 
getToken()147     public IBinder getToken() {
148         return mStateManager.getToken();
149     }
150 
setBindRequested(boolean bindRequested)151     public void setBindRequested(boolean bindRequested) {
152         if (mBindRequested == bindRequested) return;
153         mBindRequested = bindRequested;
154         if (mBindAllowed && mBindRequested && !mBound) {
155             mHandler.removeCallbacks(mUnbind);
156             bindService();
157         } else {
158             mServices.recalculateBindAllowance();
159         }
160         if (mBound && !mBindRequested) {
161             mHandler.postDelayed(mUnbind, UNBIND_DELAY);
162         }
163     }
164 
setLastUpdate(long lastUpdate)165     public void setLastUpdate(long lastUpdate) {
166         mLastUpdate = lastUpdate;
167         if (mBound && isActiveTile()) {
168             mStateManager.onStopListening();
169             setBindRequested(false);
170         }
171         mServices.recalculateBindAllowance();
172     }
173 
handleDestroy()174     public void handleDestroy() {
175         setBindAllowed(false);
176         mServices.getContext().unregisterReceiver(mUninstallReceiver);
177         mStateManager.handleDestroy();
178     }
179 
setBindAllowed(boolean allowed)180     public void setBindAllowed(boolean allowed) {
181         if (mBindAllowed == allowed) return;
182         mBindAllowed = allowed;
183         if (!mBindAllowed && mBound) {
184             unbindService();
185         } else if (mBindAllowed && mBindRequested && !mBound) {
186             bindService();
187         }
188     }
189 
hasPendingBind()190     public boolean hasPendingBind() {
191         return mPendingBind;
192     }
193 
clearPendingBind()194     public void clearPendingBind() {
195         mPendingBind = false;
196     }
197 
bindService()198     private void bindService() {
199         if (mBound) {
200             Log.e(TAG, "Service already bound");
201             return;
202         }
203         mPendingBind = true;
204         mBound = true;
205         mJustBound = true;
206         mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
207         mStateManager.executeSetBindService(true);
208     }
209 
unbindService()210     private void unbindService() {
211         if (!mBound) {
212             Log.e(TAG, "Service not bound");
213             return;
214         }
215         mBound = false;
216         mJustBound = false;
217         mStateManager.executeSetBindService(false);
218     }
219 
calculateBindPriority(long currentTime)220     public void calculateBindPriority(long currentTime) {
221         if (mStateManager.hasPendingClick()) {
222             // Pending click is the most important thing, need to put this service at the top of
223             // the list to be bound.
224             mPriority = Integer.MAX_VALUE;
225         } else if (mShowingDialog) {
226             // Hang on to services that are showing dialogs so they don't die.
227             mPriority = Integer.MAX_VALUE - 1;
228         } else if (mJustBound) {
229             // If we just bound, lets not thrash on binding/unbinding too much, this is second most
230             // important.
231             mPriority = Integer.MAX_VALUE - 2;
232         } else if (!mBindRequested) {
233             // Don't care about binding right now, put us last.
234             mPriority = Integer.MIN_VALUE;
235         } else {
236             // Order based on whether this was just updated.
237             long timeSinceUpdate = currentTime - mLastUpdate;
238             // Fit compare into integer space for simplicity. Make sure to leave MAX_VALUE and
239             // MAX_VALUE - 1 for the more important states above.
240             if (timeSinceUpdate > Integer.MAX_VALUE - 3) {
241                 mPriority = Integer.MAX_VALUE - 3;
242             } else {
243                 mPriority = (int) timeSinceUpdate;
244             }
245         }
246     }
247 
getBindPriority()248     public int getBindPriority() {
249         return mPriority;
250     }
251 
252     private final Runnable mUnbind = new Runnable() {
253         @Override
254         public void run() {
255             if (mBound && !mBindRequested) {
256                 unbindService();
257             }
258         }
259     };
260 
261     @VisibleForTesting
262     final Runnable mJustBoundOver = new Runnable() {
263         @Override
264         public void run() {
265             mJustBound = false;
266             mServices.recalculateBindAllowance();
267         }
268     };
269 
270     private final BroadcastReceiver mUninstallReceiver = new BroadcastReceiver() {
271         @Override
272         public void onReceive(Context context, Intent intent) {
273             if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
274                 return;
275             }
276 
277             Uri data = intent.getData();
278             String pkgName = data.getEncodedSchemeSpecificPart();
279             final ComponentName component = mStateManager.getComponent();
280             if (!Objects.equals(pkgName, component.getPackageName())) {
281                 return;
282             }
283 
284             // If the package is being updated, verify the component still exists.
285             if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
286                 Intent queryIntent = new Intent(TileService.ACTION_QS_TILE);
287                 queryIntent.setPackage(pkgName);
288                 PackageManager pm = context.getPackageManager();
289                 List<ResolveInfo> services = pm.queryIntentServicesAsUser(
290                         queryIntent, 0, mUserTracker.getUserId());
291                 for (ResolveInfo info : services) {
292                     if (Objects.equals(info.serviceInfo.packageName, component.getPackageName())
293                             && Objects.equals(info.serviceInfo.name, component.getClassName())) {
294                         return;
295                     }
296                 }
297             }
298 
299             mServices.getHost().removeTile(CustomTile.toSpec(component));
300         }
301     };
302 }
303