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