blob: da2c5245b1a845f8daabdb39c271467bfbd3c181 [file] [log] [blame]
/*
* 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.ActivityManager;
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.ResolveInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
import android.service.quicksettings.IQSTileService;
import android.service.quicksettings.Tile;
import android.service.quicksettings.TileService;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.systemui.qs.external.TileLifecycleManager.TileChangeListener;
import java.util.List;
import libcore.util.Objects;
/**
* Manages the priority which lets {@link TileServices} make decisions about which tiles
* to bind. Also holds on to and manages the {@link TileLifecycleManager}, informing it
* of when it is allowed to bind based on decisions frome the {@link TileServices}.
*/
public class TileServiceManager {
private static final long MIN_BIND_TIME = 5000;
private static final long UNBIND_DELAY = 30000;
public static final boolean DEBUG = true;
private static final String TAG = "TileServiceManager";
@VisibleForTesting
static final String PREFS_FILE = "CustomTileModes";
private final TileServices mServices;
private final TileLifecycleManager mStateManager;
private final Handler mHandler;
private boolean mBindRequested;
private boolean mBindAllowed;
private boolean mBound;
private int mPriority;
private boolean mJustBound;
private long mLastUpdate;
private boolean mShowingDialog;
// Whether we have a pending bind going out to the service without a response yet.
// This defaults to true to ensure tiles start out unavailable.
private boolean mPendingBind = true;
TileServiceManager(TileServices tileServices, Handler handler, ComponentName component,
Tile tile) {
this(tileServices, handler, new TileLifecycleManager(handler,
tileServices.getContext(), tileServices, tile, new Intent().setComponent(component),
new UserHandle(ActivityManager.getCurrentUser())));
}
@VisibleForTesting
TileServiceManager(TileServices tileServices, Handler handler,
TileLifecycleManager tileLifecycleManager) {
mServices = tileServices;
mHandler = handler;
mStateManager = tileLifecycleManager;
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addDataScheme("package");
Context context = mServices.getContext();
context.registerReceiverAsUser(mUninstallReceiver,
new UserHandle(ActivityManager.getCurrentUser()), filter, null, mHandler);
ComponentName component = tileLifecycleManager.getComponent();
if (!TileLifecycleManager.isTileAdded(context, component)) {
TileLifecycleManager.setTileAdded(context, component, true);
mStateManager.onTileAdded();
mStateManager.flushMessagesAndUnbind();
}
}
public void setTileChangeListener(TileChangeListener changeListener) {
mStateManager.setTileChangeListener(changeListener);
}
public boolean isActiveTile() {
return mStateManager.isActiveTile();
}
public void setShowingDialog(boolean dialog) {
mShowingDialog = dialog;
}
public IQSTileService getTileService() {
return mStateManager;
}
public void setBindRequested(boolean bindRequested) {
if (mBindRequested == bindRequested) return;
mBindRequested = bindRequested;
if (mBindAllowed && mBindRequested && !mBound) {
mHandler.removeCallbacks(mUnbind);
bindService();
} else {
mServices.recalculateBindAllowance();
}
if (mBound && !mBindRequested) {
mHandler.postDelayed(mUnbind, UNBIND_DELAY);
}
}
public void setLastUpdate(long lastUpdate) {
mLastUpdate = lastUpdate;
if (mBound && isActiveTile()) {
mStateManager.onStopListening();
setBindRequested(false);
}
mServices.recalculateBindAllowance();
}
public void handleDestroy() {
mServices.getContext().unregisterReceiver(mUninstallReceiver);
mStateManager.handleDestroy();
}
public void setBindAllowed(boolean allowed) {
if (mBindAllowed == allowed) return;
mBindAllowed = allowed;
if (!mBindAllowed && mBound) {
unbindService();
} else if (mBindAllowed && mBindRequested && !mBound) {
bindService();
}
}
public boolean hasPendingBind() {
return mPendingBind;
}
public void clearPendingBind() {
mPendingBind = false;
}
private void bindService() {
if (mBound) {
Log.e(TAG, "Service already bound");
return;
}
mPendingBind = true;
mBound = true;
mJustBound = true;
mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
mStateManager.setBindService(true);
}
private void unbindService() {
if (!mBound) {
Log.e(TAG, "Service not bound");
return;
}
mBound = false;
mJustBound = false;
mStateManager.setBindService(false);
}
public void calculateBindPriority(long currentTime) {
if (mStateManager.hasPendingClick()) {
// Pending click is the most important thing, need to put this service at the top of
// the list to be bound.
mPriority = Integer.MAX_VALUE;
} else if (mShowingDialog) {
// Hang on to services that are showing dialogs so they don't die.
mPriority = Integer.MAX_VALUE - 1;
} else if (mJustBound) {
// If we just bound, lets not thrash on binding/unbinding too much, this is second most
// important.
mPriority = Integer.MAX_VALUE - 2;
} else if (!mBindRequested) {
// Don't care about binding right now, put us last.
mPriority = Integer.MIN_VALUE;
} else {
// Order based on whether this was just updated.
long timeSinceUpdate = currentTime - mLastUpdate;
// Fit compare into integer space for simplicity. Make sure to leave MAX_VALUE and
// MAX_VALUE - 1 for the more important states above.
if (timeSinceUpdate > Integer.MAX_VALUE - 3) {
mPriority = Integer.MAX_VALUE - 3;
} else {
mPriority = (int) timeSinceUpdate;
}
}
}
public int getBindPriority() {
return mPriority;
}
private final Runnable mUnbind = new Runnable() {
@Override
public void run() {
if (mBound && !mBindRequested) {
unbindService();
}
}
};
@VisibleForTesting
final Runnable mJustBoundOver = new Runnable() {
@Override
public void run() {
mJustBound = false;
mServices.recalculateBindAllowance();
}
};
private final BroadcastReceiver mUninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
return;
}
Uri data = intent.getData();
String pkgName = data.getEncodedSchemeSpecificPart();
final ComponentName component = mStateManager.getComponent();
if (!Objects.equal(pkgName, component.getPackageName())) {
return;
}
// If the package is being updated, verify the component still exists.
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
Intent queryIntent = new Intent(TileService.ACTION_QS_TILE);
queryIntent.setPackage(pkgName);
PackageManager pm = context.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
queryIntent, 0, ActivityManager.getCurrentUser());
for (ResolveInfo info : services) {
if (Objects.equal(info.serviceInfo.packageName, component.getPackageName())
&& Objects.equal(info.serviceInfo.name, component.getClassName())) {
return;
}
}
}
mServices.getHost().removeTile(component);
}
};
}