• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.ServiceConnection;
24 import android.content.pm.PackageManager;
25 import android.content.pm.ServiceInfo;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.os.UserHandle;
32 import android.service.quicksettings.IQSService;
33 import android.service.quicksettings.IQSTileService;
34 import android.service.quicksettings.Tile;
35 import android.service.quicksettings.TileService;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import androidx.annotation.VisibleForTesting;
40 
41 import com.android.systemui.broadcast.BroadcastDispatcher;
42 
43 import java.util.NoSuchElementException;
44 import java.util.Objects;
45 import java.util.Set;
46 import java.util.concurrent.atomic.AtomicBoolean;
47 
48 /**
49  * Manages the lifecycle of a TileService.
50  * <p>
51  * Will keep track of all calls on the IQSTileService interface and will relay those calls to the
52  * TileService as soon as it is bound.  It will only bind to the service when it is allowed to
53  * ({@link #setBindService(boolean)}) and when the service is available.
54  */
55 public class TileLifecycleManager extends BroadcastReceiver implements
56         IQSTileService, ServiceConnection, IBinder.DeathRecipient {
57     public static final boolean DEBUG = false;
58 
59     private static final String TAG = "TileLifecycleManager";
60 
61     private static final int META_DATA_QUERY_FLAGS =
62             PackageManager.GET_META_DATA
63                     | PackageManager.MATCH_UNINSTALLED_PACKAGES
64                     | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
65                     | PackageManager.MATCH_DIRECT_BOOT_AWARE;
66 
67     private static final int MSG_ON_ADDED = 0;
68     private static final int MSG_ON_REMOVED = 1;
69     private static final int MSG_ON_CLICK = 2;
70     private static final int MSG_ON_UNLOCK_COMPLETE = 3;
71 
72     // Bind retry control.
73     private static final int MAX_BIND_RETRIES = 5;
74     private static final int DEFAULT_BIND_RETRY_DELAY = 1000;
75 
76     // Shared prefs that hold tile lifecycle info.
77     private static final String TILES = "tiles_prefs";
78 
79     private final Context mContext;
80     private final Handler mHandler;
81     private final Intent mIntent;
82     private final UserHandle mUser;
83     private final IBinder mToken = new Binder();
84     private final PackageManagerAdapter mPackageManagerAdapter;
85     private final BroadcastDispatcher mBroadcastDispatcher;
86 
87     private Set<Integer> mQueuedMessages = new ArraySet<>();
88     private QSTileServiceWrapper mWrapper;
89     private boolean mListening;
90     private IBinder mClickBinder;
91 
92     private int mBindTryCount;
93     private int mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
94     private boolean mBound;
95     private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false);
96     private AtomicBoolean mUserReceiverRegistered = new AtomicBoolean(false);
97     private boolean mUnbindImmediate;
98     private TileChangeListener mChangeListener;
99     // Return value from bindServiceAsUser, determines whether safe to call unbind.
100     private boolean mIsBound;
101 
TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile, Intent intent, UserHandle user, BroadcastDispatcher broadcastDispatcher)102     public TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile,
103             Intent intent, UserHandle user, BroadcastDispatcher broadcastDispatcher) {
104         this(handler, context, service, tile, intent, user, new PackageManagerAdapter(context),
105                 broadcastDispatcher);
106     }
107 
108     @VisibleForTesting
TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile, Intent intent, UserHandle user, PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher)109     TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile,
110             Intent intent, UserHandle user, PackageManagerAdapter packageManagerAdapter,
111             BroadcastDispatcher broadcastDispatcher) {
112         mContext = context;
113         mHandler = handler;
114         mIntent = intent;
115         mIntent.putExtra(TileService.EXTRA_SERVICE, service.asBinder());
116         mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
117         mUser = user;
118         mPackageManagerAdapter = packageManagerAdapter;
119         mBroadcastDispatcher = broadcastDispatcher;
120         if (DEBUG) Log.d(TAG, "Creating " + mIntent + " " + mUser);
121     }
122 
getComponent()123     public ComponentName getComponent() {
124         return mIntent.getComponent();
125     }
126 
hasPendingClick()127     public boolean hasPendingClick() {
128         synchronized (mQueuedMessages) {
129             return mQueuedMessages.contains(MSG_ON_CLICK);
130         }
131     }
132 
setBindRetryDelay(int delayMs)133     public void setBindRetryDelay(int delayMs) {
134         mBindRetryDelay = delayMs;
135     }
136 
isActiveTile()137     public boolean isActiveTile() {
138         try {
139             ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
140                     META_DATA_QUERY_FLAGS);
141             return info.metaData != null
142                     && info.metaData.getBoolean(TileService.META_DATA_ACTIVE_TILE, false);
143         } catch (PackageManager.NameNotFoundException e) {
144             return false;
145         }
146     }
147 
148     /**
149      * Determines whether the associated TileService is a Boolean Tile.
150      *
151      * @return true if {@link TileService#META_DATA_TOGGLEABLE_TILE} is set to {@code true} for this
152      *         tile
153      * @see TileService#META_DATA_TOGGLEABLE_TILE
154      */
isToggleableTile()155     public boolean isToggleableTile() {
156         try {
157             ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
158                     META_DATA_QUERY_FLAGS);
159             return info.metaData != null
160                     && info.metaData.getBoolean(TileService.META_DATA_TOGGLEABLE_TILE, false);
161         } catch (PackageManager.NameNotFoundException e) {
162             return false;
163         }
164     }
165 
166     /**
167      * Binds just long enough to send any queued messages, then unbinds.
168      */
flushMessagesAndUnbind()169     public void flushMessagesAndUnbind() {
170         mUnbindImmediate = true;
171         setBindService(true);
172     }
173 
setBindService(boolean bind)174     public void setBindService(boolean bind) {
175         if (mBound && mUnbindImmediate) {
176             // If we are already bound and expecting to unbind, this means we should stay bound
177             // because something else wants to hold the connection open.
178             mUnbindImmediate = false;
179             return;
180         }
181         mBound = bind;
182         if (bind) {
183             if (mBindTryCount == MAX_BIND_RETRIES) {
184                 // Too many failures, give up on this tile until an update.
185                 startPackageListening();
186                 return;
187             }
188             if (!checkComponentState()) {
189                 return;
190             }
191             if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
192             mBindTryCount++;
193             try {
194                 mIsBound = mContext.bindServiceAsUser(mIntent, this,
195                         Context.BIND_AUTO_CREATE
196                                 | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE
197                                 | Context.BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS
198                                 | Context.BIND_WAIVE_PRIORITY,
199                         mUser);
200             } catch (SecurityException e) {
201                 Log.e(TAG, "Failed to bind to service", e);
202                 mIsBound = false;
203             }
204         } else {
205             if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser);
206             // Give it another chance next time it needs to be bound, out of kindness.
207             mBindTryCount = 0;
208             freeWrapper();
209             if (mIsBound) {
210                 try {
211                     mContext.unbindService(this);
212                 } catch (Exception e) {
213                     Log.e(TAG, "Failed to unbind service "
214                             + mIntent.getComponent().flattenToShortString(), e);
215                 }
216                 mIsBound = false;
217             }
218         }
219     }
220 
221     @Override
onServiceConnected(ComponentName name, IBinder service)222     public void onServiceConnected(ComponentName name, IBinder service) {
223         if (DEBUG) Log.d(TAG, "onServiceConnected " + name);
224         // Got a connection, set the binding count to 0.
225         mBindTryCount = 0;
226         final QSTileServiceWrapper wrapper = new QSTileServiceWrapper(Stub.asInterface(service));
227         try {
228             service.linkToDeath(this, 0);
229         } catch (RemoteException e) {
230         }
231         mWrapper = wrapper;
232         handlePendingMessages();
233     }
234 
235     @Override
onServiceDisconnected(ComponentName name)236     public void onServiceDisconnected(ComponentName name) {
237         if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
238         handleDeath();
239     }
240 
handlePendingMessages()241     private void handlePendingMessages() {
242         // This ordering is laid out manually to make sure we preserve the TileService
243         // lifecycle.
244         ArraySet<Integer> queue;
245         synchronized (mQueuedMessages) {
246             queue = new ArraySet<>(mQueuedMessages);
247             mQueuedMessages.clear();
248         }
249         if (queue.contains(MSG_ON_ADDED)) {
250             if (DEBUG) Log.d(TAG, "Handling pending onAdded");
251             onTileAdded();
252         }
253         if (mListening) {
254             if (DEBUG) Log.d(TAG, "Handling pending onStartListening");
255             onStartListening();
256         }
257         if (queue.contains(MSG_ON_CLICK)) {
258             if (DEBUG) Log.d(TAG, "Handling pending onClick");
259             if (!mListening) {
260                 Log.w(TAG, "Managed to get click on non-listening state...");
261                 // Skipping click since lost click privileges.
262             } else {
263                 onClick(mClickBinder);
264             }
265         }
266         if (queue.contains(MSG_ON_UNLOCK_COMPLETE)) {
267             if (DEBUG) Log.d(TAG, "Handling pending onUnlockComplete");
268             if (!mListening) {
269                 Log.w(TAG, "Managed to get unlock on non-listening state...");
270                 // Skipping unlock since lost click privileges.
271             } else {
272                 onUnlockComplete();
273             }
274         }
275         if (queue.contains(MSG_ON_REMOVED)) {
276             if (DEBUG) Log.d(TAG, "Handling pending onRemoved");
277             if (mListening) {
278                 Log.w(TAG, "Managed to get remove in listening state...");
279                 onStopListening();
280             }
281             onTileRemoved();
282         }
283         if (mUnbindImmediate) {
284             mUnbindImmediate = false;
285             setBindService(false);
286         }
287     }
288 
handleDestroy()289     public void handleDestroy() {
290         if (DEBUG) Log.d(TAG, "handleDestroy");
291         if (mPackageReceiverRegistered.get() || mUserReceiverRegistered.get()) {
292             stopPackageListening();
293         }
294         mChangeListener = null;
295     }
296 
handleDeath()297     private void handleDeath() {
298         if (mWrapper == null) return;
299         freeWrapper();
300         // Clearly not bound anymore
301         mIsBound = false;
302         if (!mBound) return;
303         if (DEBUG) Log.d(TAG, "handleDeath");
304         if (checkComponentState()) {
305             mHandler.postDelayed(new Runnable() {
306                 @Override
307                 public void run() {
308                     if (mBound) {
309                         // Retry binding.
310                         setBindService(true);
311                     }
312                 }
313             }, mBindRetryDelay);
314         }
315     }
316 
checkComponentState()317     private boolean checkComponentState() {
318         if (!isPackageAvailable() || !isComponentAvailable()) {
319             startPackageListening();
320             return false;
321         }
322         return true;
323     }
324 
startPackageListening()325     private void startPackageListening() {
326         if (DEBUG) Log.d(TAG, "startPackageListening");
327         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
328         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
329         filter.addDataScheme("package");
330         try {
331             mPackageReceiverRegistered.set(true);
332             mContext.registerReceiverAsUser(this, mUser, filter, null, mHandler);
333         } catch (Exception ex) {
334             mPackageReceiverRegistered.set(false);
335             Log.e(TAG, "Could not register package receiver", ex);
336         }
337         filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
338         try {
339             mUserReceiverRegistered.set(true);
340             mBroadcastDispatcher.registerReceiverWithHandler(this, filter, mHandler, mUser);
341         } catch (Exception ex) {
342             mUserReceiverRegistered.set(false);
343             Log.e(TAG, "Could not register unlock receiver", ex);
344         }
345     }
346 
stopPackageListening()347     private void stopPackageListening() {
348         if (DEBUG) Log.d(TAG, "stopPackageListening");
349         if (mUserReceiverRegistered.compareAndSet(true, false)) {
350             mBroadcastDispatcher.unregisterReceiver(this);
351         }
352         if (mPackageReceiverRegistered.compareAndSet(true, false)) {
353             mContext.unregisterReceiver(this);
354         }
355     }
356 
setTileChangeListener(TileChangeListener changeListener)357     public void setTileChangeListener(TileChangeListener changeListener) {
358         mChangeListener = changeListener;
359     }
360 
361     @Override
onReceive(Context context, Intent intent)362     public void onReceive(Context context, Intent intent) {
363         if (DEBUG) Log.d(TAG, "onReceive: " + intent);
364         if (!Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
365             Uri data = intent.getData();
366             String pkgName = data.getEncodedSchemeSpecificPart();
367             if (!Objects.equals(pkgName, mIntent.getComponent().getPackageName())) {
368                 return;
369             }
370         }
371         if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) && mChangeListener != null) {
372             mChangeListener.onTileChanged(mIntent.getComponent());
373         }
374         stopPackageListening();
375         if (mBound) {
376             // Trying to bind again will check the state of the package before bothering to bind.
377             if (DEBUG) Log.d(TAG, "Trying to rebind");
378             setBindService(true);
379         }
380     }
381 
isComponentAvailable()382     private boolean isComponentAvailable() {
383         String packageName = mIntent.getComponent().getPackageName();
384         try {
385             ServiceInfo si = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
386                     0, mUser.getIdentifier());
387             if (DEBUG && si == null) Log.d(TAG, "Can't find component " + mIntent.getComponent());
388             return si != null;
389         } catch (RemoteException e) {
390             // Shouldn't happen.
391         }
392         return false;
393     }
394 
isPackageAvailable()395     private boolean isPackageAvailable() {
396         String packageName = mIntent.getComponent().getPackageName();
397         try {
398             mPackageManagerAdapter.getPackageInfoAsUser(packageName, 0, mUser.getIdentifier());
399             return true;
400         } catch (PackageManager.NameNotFoundException e) {
401             if (DEBUG) Log.d(TAG, "Package not available: " + packageName, e);
402             else Log.d(TAG, "Package not available: " + packageName);
403         }
404         return false;
405     }
406 
queueMessage(int message)407     private void queueMessage(int message) {
408         synchronized (mQueuedMessages) {
409             mQueuedMessages.add(message);
410         }
411     }
412 
413     @Override
onTileAdded()414     public void onTileAdded() {
415         if (DEBUG) Log.d(TAG, "onTileAdded");
416         if (mWrapper == null || !mWrapper.onTileAdded()) {
417             queueMessage(MSG_ON_ADDED);
418             handleDeath();
419         }
420     }
421 
422     @Override
onTileRemoved()423     public void onTileRemoved() {
424         if (DEBUG) Log.d(TAG, "onTileRemoved");
425         if (mWrapper == null || !mWrapper.onTileRemoved()) {
426             queueMessage(MSG_ON_REMOVED);
427             handleDeath();
428         }
429     }
430 
431     @Override
onStartListening()432     public void onStartListening() {
433         if (DEBUG) Log.d(TAG, "onStartListening");
434         mListening = true;
435         if (mWrapper != null && !mWrapper.onStartListening()) {
436             handleDeath();
437         }
438     }
439 
440     @Override
onStopListening()441     public void onStopListening() {
442         if (DEBUG) Log.d(TAG, "onStopListening");
443         mListening = false;
444         if (mWrapper != null && !mWrapper.onStopListening()) {
445             handleDeath();
446         }
447     }
448 
449     @Override
onClick(IBinder iBinder)450     public void onClick(IBinder iBinder) {
451         if (DEBUG) Log.d(TAG, "onClick " + iBinder + " " + mUser);
452         if (mWrapper == null || !mWrapper.onClick(iBinder)) {
453             mClickBinder = iBinder;
454             queueMessage(MSG_ON_CLICK);
455             handleDeath();
456         }
457     }
458 
459     @Override
onUnlockComplete()460     public void onUnlockComplete() {
461         if (DEBUG) Log.d(TAG, "onUnlockComplete");
462         if (mWrapper == null || !mWrapper.onUnlockComplete()) {
463             queueMessage(MSG_ON_UNLOCK_COMPLETE);
464             handleDeath();
465         }
466     }
467 
468     @Override
asBinder()469     public IBinder asBinder() {
470         return mWrapper != null ? mWrapper.asBinder() : null;
471     }
472 
473     @Override
binderDied()474     public void binderDied() {
475         if (DEBUG) Log.d(TAG, "binderDeath");
476         handleDeath();
477     }
478 
getToken()479     public IBinder getToken() {
480         return mToken;
481     }
482 
freeWrapper()483     private void freeWrapper() {
484         if (mWrapper != null) {
485             try {
486                 mWrapper.asBinder().unlinkToDeath(this, 0);
487             } catch (NoSuchElementException e) {
488                 Log.w(TAG, "Trying to unlink not linked recipient for component"
489                         + mIntent.getComponent().flattenToShortString());
490             }
491             mWrapper = null;
492         }
493     }
494 
495     public interface TileChangeListener {
onTileChanged(ComponentName tile)496         void onTileChanged(ComponentName tile);
497     }
498 
isTileAdded(Context context, ComponentName component)499     public static boolean isTileAdded(Context context, ComponentName component) {
500         return context.getSharedPreferences(TILES, 0).getBoolean(component.flattenToString(), false);
501     }
502 
setTileAdded(Context context, ComponentName component, boolean added)503     public static void setTileAdded(Context context, ComponentName component, boolean added) {
504         context.getSharedPreferences(TILES, 0).edit().putBoolean(component.flattenToString(),
505                 added).commit();
506     }
507 }
508