/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.qs.external;
import android.app.AppGlobals;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.service.quicksettings.IQSService;
import android.service.quicksettings.IQSTileService;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.support.annotation.VisibleForTesting;
import android.util.ArraySet;
import android.util.Log;
import com.android.systemui.qs.external.PackageManagerAdapter;
import libcore.util.Objects;
import java.util.Set;
/**
* Manages the lifecycle of a TileService.
*
* Will keep track of all calls on the IQSTileService interface and will relay those calls to the
* TileService as soon as it is bound. It will only bind to the service when it is allowed to
* ({@link #setBindService(boolean)}) and when the service is available.
*/
public class TileLifecycleManager extends BroadcastReceiver implements
IQSTileService, ServiceConnection, IBinder.DeathRecipient {
public static final boolean DEBUG = false;
private static final String TAG = "TileLifecycleManager";
private static final int MSG_ON_ADDED = 0;
private static final int MSG_ON_REMOVED = 1;
private static final int MSG_ON_CLICK = 2;
private static final int MSG_ON_UNLOCK_COMPLETE = 3;
// Bind retry control.
private static final int MAX_BIND_RETRIES = 5;
private static final int DEFAULT_BIND_RETRY_DELAY = 1000;
// Shared prefs that hold tile lifecycle info.
private static final String TILES = "tiles_prefs";
private final Context mContext;
private final Handler mHandler;
private final Intent mIntent;
private final UserHandle mUser;
private final IBinder mToken = new Binder();
private final PackageManagerAdapter mPackageManagerAdapter;
private Set mQueuedMessages = new ArraySet<>();
private QSTileServiceWrapper mWrapper;
private boolean mListening;
private IBinder mClickBinder;
private int mBindTryCount;
private int mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
private boolean mBound;
boolean mReceiverRegistered;
private boolean mUnbindImmediate;
private TileChangeListener mChangeListener;
// Return value from bindServiceAsUser, determines whether safe to call unbind.
private boolean mIsBound;
public TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile,
Intent intent, UserHandle user) {
this(handler, context, service, tile, intent, user, new PackageManagerAdapter(context));
}
@VisibleForTesting
TileLifecycleManager(Handler handler, Context context, IQSService service, Tile tile,
Intent intent, UserHandle user, PackageManagerAdapter packageManagerAdapter) {
mContext = context;
mHandler = handler;
mIntent = intent;
mIntent.putExtra(TileService.EXTRA_SERVICE, service.asBinder());
mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
mUser = user;
mPackageManagerAdapter = packageManagerAdapter;
if (DEBUG) Log.d(TAG, "Creating " + mIntent + " " + mUser);
}
public ComponentName getComponent() {
return mIntent.getComponent();
}
public boolean hasPendingClick() {
synchronized (mQueuedMessages) {
return mQueuedMessages.contains(MSG_ON_CLICK);
}
}
public void setBindRetryDelay(int delayMs) {
mBindRetryDelay = delayMs;
}
public boolean isActiveTile() {
try {
ServiceInfo info = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.GET_META_DATA);
return info.metaData != null
&& info.metaData.getBoolean(TileService.META_DATA_ACTIVE_TILE, false);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* Binds just long enough to send any queued messages, then unbinds.
*/
public void flushMessagesAndUnbind() {
mUnbindImmediate = true;
setBindService(true);
}
public void setBindService(boolean bind) {
if (mBound && mUnbindImmediate) {
// If we are already bound and expecting to unbind, this means we should stay bound
// because something else wants to hold the connection open.
mUnbindImmediate = false;
return;
}
mBound = bind;
if (bind) {
if (mBindTryCount == MAX_BIND_RETRIES) {
// Too many failures, give up on this tile until an update.
startPackageListening();
return;
}
if (!checkComponentState()) {
return;
}
if (DEBUG) Log.d(TAG, "Binding service " + mIntent + " " + mUser);
mBindTryCount++;
try {
mIsBound = mContext.bindServiceAsUser(mIntent, this,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
mUser);
} catch (SecurityException e) {
Log.e(TAG, "Failed to bind to service", e);
mIsBound = false;
}
} else {
if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent + " " + mUser);
// Give it another chance next time it needs to be bound, out of kindness.
mBindTryCount = 0;
mWrapper = null;
if (mIsBound) {
mContext.unbindService(this);
mIsBound = false;
}
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (DEBUG) Log.d(TAG, "onServiceConnected " + name);
// Got a connection, set the binding count to 0.
mBindTryCount = 0;
final QSTileServiceWrapper wrapper = new QSTileServiceWrapper(Stub.asInterface(service));
try {
service.linkToDeath(this, 0);
} catch (RemoteException e) {
}
mWrapper = wrapper;
handlePendingMessages();
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
handleDeath();
}
private void handlePendingMessages() {
// This ordering is laid out manually to make sure we preserve the TileService
// lifecycle.
ArraySet queue;
synchronized (mQueuedMessages) {
queue = new ArraySet<>(mQueuedMessages);
mQueuedMessages.clear();
}
if (queue.contains(MSG_ON_ADDED)) {
if (DEBUG) Log.d(TAG, "Handling pending onAdded");
onTileAdded();
}
if (mListening) {
if (DEBUG) Log.d(TAG, "Handling pending onStartListening");
onStartListening();
}
if (queue.contains(MSG_ON_CLICK)) {
if (DEBUG) Log.d(TAG, "Handling pending onClick");
if (!mListening) {
Log.w(TAG, "Managed to get click on non-listening state...");
// Skipping click since lost click privileges.
} else {
onClick(mClickBinder);
}
}
if (queue.contains(MSG_ON_UNLOCK_COMPLETE)) {
if (DEBUG) Log.d(TAG, "Handling pending onUnlockComplete");
if (!mListening) {
Log.w(TAG, "Managed to get unlock on non-listening state...");
// Skipping unlock since lost click privileges.
} else {
onUnlockComplete();
}
}
if (queue.contains(MSG_ON_REMOVED)) {
if (DEBUG) Log.d(TAG, "Handling pending onRemoved");
if (mListening) {
Log.w(TAG, "Managed to get remove in listening state...");
onStopListening();
}
onTileRemoved();
}
if (mUnbindImmediate) {
mUnbindImmediate = false;
setBindService(false);
}
}
public void handleDestroy() {
if (DEBUG) Log.d(TAG, "handleDestroy");
if (mReceiverRegistered) {
stopPackageListening();
}
}
private void handleDeath() {
if (mWrapper == null) return;
mWrapper = null;
if (!mBound) return;
if (DEBUG) Log.d(TAG, "handleDeath");
if (checkComponentState()) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (mBound) {
// Retry binding.
setBindService(true);
}
}
}, mBindRetryDelay);
}
}
private boolean checkComponentState() {
if (!isPackageAvailable() || !isComponentAvailable()) {
startPackageListening();
return false;
}
return true;
}
private void startPackageListening() {
if (DEBUG) Log.d(TAG, "startPackageListening");
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addDataScheme("package");
mContext.registerReceiverAsUser(this, mUser, filter, null, mHandler);
filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
mContext.registerReceiverAsUser(this, mUser, filter, null, mHandler);
mReceiverRegistered = true;
}
private void stopPackageListening() {
if (DEBUG) Log.d(TAG, "stopPackageListening");
mContext.unregisterReceiver(this);
mReceiverRegistered = false;
}
public void setTileChangeListener(TileChangeListener changeListener) {
mChangeListener = changeListener;
}
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "onReceive: " + intent);
if (!Intent.ACTION_USER_UNLOCKED.equals(intent.getAction())) {
Uri data = intent.getData();
String pkgName = data.getEncodedSchemeSpecificPart();
if (!Objects.equal(pkgName, mIntent.getComponent().getPackageName())) {
return;
}
}
if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction()) && mChangeListener != null) {
mChangeListener.onTileChanged(mIntent.getComponent());
}
stopPackageListening();
if (mBound) {
// Trying to bind again will check the state of the package before bothering to bind.
if (DEBUG) Log.d(TAG, "Trying to rebind");
setBindService(true);
}
}
private boolean isComponentAvailable() {
String packageName = mIntent.getComponent().getPackageName();
try {
ServiceInfo si = mPackageManagerAdapter.getServiceInfo(mIntent.getComponent(),
0, mUser.getIdentifier());
if (DEBUG && si == null) Log.d(TAG, "Can't find component " + mIntent.getComponent());
return si != null;
} catch (RemoteException e) {
// Shouldn't happen.
}
return false;
}
private boolean isPackageAvailable() {
String packageName = mIntent.getComponent().getPackageName();
try {
mPackageManagerAdapter.getPackageInfoAsUser(packageName, 0, mUser.getIdentifier());
return true;
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Package not available: " + packageName, e);
else Log.d(TAG, "Package not available: " + packageName);
}
return false;
}
private void queueMessage(int message) {
synchronized (mQueuedMessages) {
mQueuedMessages.add(message);
}
}
@Override
public void onTileAdded() {
if (DEBUG) Log.d(TAG, "onTileAdded");
if (mWrapper == null || !mWrapper.onTileAdded()) {
queueMessage(MSG_ON_ADDED);
handleDeath();
}
}
@Override
public void onTileRemoved() {
if (DEBUG) Log.d(TAG, "onTileRemoved");
if (mWrapper == null || !mWrapper.onTileRemoved()) {
queueMessage(MSG_ON_REMOVED);
handleDeath();
}
}
@Override
public void onStartListening() {
if (DEBUG) Log.d(TAG, "onStartListening");
mListening = true;
if (mWrapper != null && !mWrapper.onStartListening()) {
handleDeath();
}
}
@Override
public void onStopListening() {
if (DEBUG) Log.d(TAG, "onStopListening");
mListening = false;
if (mWrapper != null && !mWrapper.onStopListening()) {
handleDeath();
}
}
@Override
public void onClick(IBinder iBinder) {
if (DEBUG) Log.d(TAG, "onClick " + iBinder + " " + mUser);
if (mWrapper == null || !mWrapper.onClick(iBinder)) {
mClickBinder = iBinder;
queueMessage(MSG_ON_CLICK);
handleDeath();
}
}
@Override
public void onUnlockComplete() {
if (DEBUG) Log.d(TAG, "onUnlockComplete");
if (mWrapper == null || !mWrapper.onUnlockComplete()) {
queueMessage(MSG_ON_UNLOCK_COMPLETE);
handleDeath();
}
}
@Override
public IBinder asBinder() {
return mWrapper != null ? mWrapper.asBinder() : null;
}
@Override
public void binderDied() {
if (DEBUG) Log.d(TAG, "binderDeath");
handleDeath();
}
public IBinder getToken() {
return mToken;
}
public interface TileChangeListener {
void onTileChanged(ComponentName tile);
}
public static boolean isTileAdded(Context context, ComponentName component) {
return context.getSharedPreferences(TILES, 0).getBoolean(component.flattenToString(), false);
}
public static void setTileAdded(Context context, ComponentName component, boolean added) {
context.getSharedPreferences(TILES, 0).edit().putBoolean(component.flattenToString(),
added).commit();
}
}