From d5a204f16e7c71ffdbc6c8307a4134dcc1efd60d Mon Sep 17 00:00:00 2001
From: Jason Monk The lifecycle of a QSTileService is different from some other services in
+ * The lifecycle of a TileService is different from some other services in
* that it may be unbound during parts of its lifecycle. Any of the following
* lifecycle events can happen indepently in a separate binding/creation of the
* service. QSTileService will be detected by tiles that match the {@value #ACTION_QS_TILE}
+ * TileService will be detected by tiles that match the {@value #ACTION_QS_TILE}
* and require the permission "android.permission.BIND_QUICK_SETTINGS_TILE".
* The label and icon for the service will be used as the default label and
- * icon for the tile. Here is an example QSTileService declaration.
- *
- *
* {@literal
* Should only be requested by the System, should be required by
- QSTileService declarations.-->
+ TileService declarations.-->
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileServiceWrapper.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileServiceWrapper.java
deleted file mode 100644
index a5e1fd55c8f0..000000000000
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileServiceWrapper.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.android.systemui.qs;
-
-import android.os.IBinder;
-import android.service.quicksettings.IQSTileService;
-import android.service.quicksettings.Tile;
-import android.util.Log;
-
-
-public class QSTileServiceWrapper implements IQSTileService {
- private static final String TAG = "IQSTileServiceWrapper";
-
- private final IQSTileService mService;
-
- public QSTileServiceWrapper(IQSTileService service) {
- mService = service;
- }
-
- @Override
- public IBinder asBinder() {
- return mService.asBinder();
- }
-
- @Override
- public void setQSTile(Tile tile) {
- try {
- mService.setQSTile(tile);
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-
- @Override
- public void onTileAdded() {
- try {
- mService.onTileAdded();
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-
- @Override
- public void onTileRemoved() {
- try {
- mService.onTileRemoved();
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-
- @Override
- public void onStartListening() {
- try {
- mService.onStartListening();
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-
- @Override
- public void onStopListening() {
- try {
- mService.onStopListening();
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-
- @Override
- public void onClick(IBinder token) {
- try {
- mService.onClick(token);
- } catch (Exception e) {
- Log.d(TAG, "Caught exception from QSTileService", e);
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
index 87c2973592b6..5ac63bc6b189 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/CustomQSPanel.java
@@ -35,8 +35,8 @@ import android.view.View;
import com.android.systemui.R;
import com.android.systemui.qs.QSPanel;
import com.android.systemui.qs.QSTile;
-import com.android.systemui.qs.QSTileServiceWrapper;
-import com.android.systemui.qs.tiles.CustomTile;
+import com.android.systemui.qs.external.QSTileServiceWrapper;
+import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.tuner.TunerService;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
index 6706c7aace12..a6a71439dbae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java
@@ -41,7 +41,7 @@ import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
import com.android.systemui.qs.QSTile.Icon;
-import com.android.systemui.qs.tiles.CustomTile;
+import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.statusbar.phone.QSTileHost;
import com.android.systemui.tuner.QSPagingSwitch;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
new file mode 100644
index 000000000000..e622e1154396
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java
@@ -0,0 +1,200 @@
+/*
+ * 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.content.ComponentName;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.service.quicksettings.IQSTileService;
+import android.service.quicksettings.Tile;
+import android.util.Log;
+import android.view.IWindowManager;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.statusbar.phone.QSTileHost;
+
+public class CustomTile extends QSTile {
+ public static final String PREFIX = "custom(";
+
+ private static final boolean DEBUG = false;
+
+ // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
+ // So instead we have a period of waiting.
+ private static final long UNBIND_DELAY = 30000;
+
+ private final ComponentName mComponent;
+ private final Tile mTile;
+ private final IWindowManager mWindowManager;
+ private final IBinder mToken = new Binder();
+ private final IQSTileService mService;
+ private final TileServiceManager mServiceManager;
+
+ private boolean mListening;
+ private boolean mBound;
+ private boolean mIsTokenGranted;
+ private boolean mIsShowingDialog;
+
+ private CustomTile(QSTileHost host, String action) {
+ super(host);
+ mWindowManager = WindowManagerGlobal.getWindowManagerService();
+ mComponent = ComponentName.unflattenFromString(action);
+ mServiceManager = host.getTileServices().getTileWrapper(this);
+ mService = mServiceManager.getTileService();
+ mTile = new Tile(mComponent, host.getTileServices());
+ try {
+ PackageManager pm = mContext.getPackageManager();
+ ServiceInfo info = pm.getServiceInfo(mComponent, 0);
+ mTile.setIcon(android.graphics.drawable.Icon
+ .createWithResource(mComponent.getPackageName(), info.icon));
+ mTile.setLabel(info.loadLabel(pm));
+ } catch (Exception e) {
+ }
+ }
+
+ public ComponentName getComponent() {
+ return mComponent;
+ }
+
+ public Tile getQsTile() {
+ return mTile;
+ }
+
+ public void updateState(Tile tile) {
+ mTile.setIcon(tile.getIcon());
+ mTile.setLabel(tile.getLabel());
+ mTile.setContentDescription(tile.getContentDescription());
+ }
+
+ public void onDialogShown() {
+ mIsShowingDialog = true;
+ }
+
+ @Override
+ public void setListening(boolean listening) {
+ if (mListening == listening) return;
+ mListening = listening;
+ try {
+ if (listening) {
+ mServiceManager.setBindRequested(true);
+ mService.setQSTile(mTile);
+ mService.onStartListening();
+ } else {
+ mService.onStopListening();
+ if (mIsTokenGranted && !mIsShowingDialog) {
+ try {
+ if (DEBUG) Log.d(TAG, "Removing token");
+ mWindowManager.removeWindowToken(mToken);
+ } catch (RemoteException e) {
+ }
+ mIsTokenGranted = false;
+ }
+ mIsShowingDialog = false;
+ mServiceManager.setBindRequested(false);
+ }
+ } catch (RemoteException e) {
+ // Called through wrapper, won't happen here.
+ }
+ }
+
+ @Override
+ protected void handleDestroy() {
+ super.handleDestroy();
+ if (mIsTokenGranted) {
+ try {
+ if (DEBUG) Log.d(TAG, "Removing token");
+ mWindowManager.removeWindowToken(mToken);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Override
+ protected State newTileState() {
+ return new State();
+ }
+
+ @Override
+ protected void handleUserSwitch(int newUserId) {
+ super.handleUserSwitch(newUserId);
+ }
+
+ @Override
+ protected void handleClick() {
+ if (mService != null) {
+ try {
+ if (DEBUG) Log.d(TAG, "Adding token");
+ mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
+ mIsTokenGranted = true;
+ } catch (RemoteException e) {
+ }
+ try {
+ mService.onClick(mToken);
+ } catch (RemoteException e) {
+ // Called through wrapper, won't happen here.
+ }
+ } else {
+ Log.e(TAG, "Click with no service " + getTileSpec());
+ }
+ MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
+ }
+
+ @Override
+ protected void handleLongClick() {
+ }
+
+ @Override
+ protected void handleUpdateState(State state, Object arg) {
+ Drawable drawable = mTile.getIcon().loadDrawable(mContext);
+ drawable.setTint(mContext.getColor(android.R.color.white));
+ state.icon = new DrawableIcon(drawable);
+ state.label = mTile.getLabel();
+ if (mTile.getContentDescription() != null) {
+ state.contentDescription = mTile.getContentDescription();
+ } else {
+ state.contentDescription = state.label;
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return MetricsLogger.QS_CUSTOM;
+ }
+
+ public static ComponentName getComponentFromSpec(String spec) {
+ final String action = spec.substring(PREFIX.length(), spec.length() - 1);
+ if (action.isEmpty()) {
+ throw new IllegalArgumentException("Empty custom tile spec action");
+ }
+ return ComponentName.unflattenFromString(action);
+ }
+
+ public static QSTile> create(QSTileHost host, String spec) {
+ if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
+ throw new IllegalArgumentException("Bad custom tile spec: " + spec);
+ }
+ final String action = spec.substring(PREFIX.length(), spec.length() - 1);
+ if (action.isEmpty()) {
+ throw new IllegalArgumentException("Empty custom tile spec action");
+ }
+ return new CustomTile(host, action);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java
new file mode 100644
index 000000000000..d656686b88a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java
@@ -0,0 +1,96 @@
+/*
+ * 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.os.IBinder;
+import android.service.quicksettings.IQSTileService;
+import android.service.quicksettings.Tile;
+import android.util.Log;
+
+
+public class QSTileServiceWrapper {
+ private static final String TAG = "IQSTileServiceWrapper";
+
+ private final IQSTileService mService;
+
+ public QSTileServiceWrapper(IQSTileService service) {
+ mService = service;
+ }
+
+ public IBinder asBinder() {
+ return mService.asBinder();
+ }
+
+ public boolean setQSTile(Tile tile) {
+ try {
+ mService.setQSTile(tile);
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+
+ public boolean onTileAdded() {
+ try {
+ mService.onTileAdded();
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+
+ public boolean onTileRemoved() {
+ try {
+ mService.onTileRemoved();
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+
+ public boolean onStartListening() {
+ try {
+ mService.onStartListening();
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+
+ public boolean onStopListening() {
+ try {
+ mService.onStopListening();
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+
+ public boolean onClick(IBinder token) {
+ try {
+ mService.onClick(token);
+ return true;
+ } catch (Exception e) {
+ Log.d(TAG, "Caught exception from TileService", e);
+ return false;
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
new file mode 100644
index 000000000000..500ee1941bd9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -0,0 +1,331 @@
+/*
+ * 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.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.quicksettings.IQSTileService;
+import android.service.quicksettings.Tile;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArraySet;
+import android.util.Log;
+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;
+
+ // Bind retry control.
+ private static final int MAX_BIND_RETRIES = 5;
+ private static final int BIND_RETRY_DELAY = 1000;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final Intent mIntent;
+ private final UserHandle mUser;
+
+ private Set mQueuedMessages = new ArraySet<>();
+ private QSTileServiceWrapper mWrapper;
+ private boolean mListening;
+ private Tile mTile;
+ private IBinder mClickBinder;
+
+ private int mBindTryCount;
+ private boolean mBound;
+ @VisibleForTesting
+ boolean mReceiverRegistered;
+
+ public TileLifecycleManager(Handler handler, Context context, Intent intent, UserHandle user) {
+ mContext = context;
+ mHandler = handler;
+ mIntent = intent;
+ mUser = user;
+ }
+
+ public boolean hasPendingClick() {
+ synchronized (mQueuedMessages) {
+ return mQueuedMessages.contains(MSG_ON_CLICK);
+ }
+ }
+
+ public void setBindService(boolean bind) {
+ 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);
+ mBindTryCount++;
+ mContext.bindServiceAsUser(mIntent, this,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+ mUser);
+ } else {
+ if (DEBUG) Log.d(TAG, "Unbinding service " + mIntent);
+ // Give it another chance next time it needs to be bound, out of kindness.
+ mBindTryCount = 0;
+ mContext.unbindService(this);
+ }
+ }
+
+ @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;
+ mWrapper = new QSTileServiceWrapper(Stub.asInterface(service));
+ try {
+ service.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ }
+ handlePendingMessages();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.d(TAG, "onServiceDisconnected " + name);
+ mWrapper = null;
+ }
+
+ 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");
+ setQSTile(mTile);
+ 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_REMOVED)) {
+ if (DEBUG) Log.d(TAG, "Handling pending onRemoved");
+ if (mListening) {
+ Log.w(TAG, "Managed to get remove in listening state...");
+ onStopListening();
+ }
+ onTileRemoved();
+ }
+ }
+
+ 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);
+ }
+ }
+ }, BIND_RETRY_DELAY);
+ }
+ }
+
+ @Override
+ public void setQSTile(Tile tile) {
+ if (DEBUG) Log.d(TAG, "setQSTile " + tile);
+ mTile = tile;
+ if (mWrapper != null && !mWrapper.setQSTile(tile)) {
+ handleDeath();
+ }
+ }
+
+ private boolean checkComponentState() {
+ PackageManager pm = mContext.getPackageManager();
+ if (!isPackageAvailable(pm) || !isComponentAvailable(pm)) {
+ 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);
+ mReceiverRegistered = true;
+ }
+
+ private void stopPackageListening() {
+ if (DEBUG) Log.d(TAG, "stopPackageListening");
+ mContext.unregisterReceiver(this);
+ mReceiverRegistered = false;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) Log.d(TAG, "onReceive: " + intent);
+ Uri data = intent.getData();
+ String pkgName = data.getEncodedSchemeSpecificPart();
+ if (!Objects.equal(pkgName, mIntent.getComponent().getPackageName())) {
+ return;
+ }
+ 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(PackageManager pm) {
+ String packageName = mIntent.getComponent().getPackageName();
+ try {
+ ServiceInfo si = AppGlobals.getPackageManager().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(PackageManager pm) {
+ String packageName = mIntent.getComponent().getPackageName();
+ try {
+ pm.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);
+ if (mWrapper == null || !mWrapper.onClick(iBinder)) {
+ mClickBinder = iBinder;
+ queueMessage(MSG_ON_CLICK);
+ handleDeath();
+ }
+ }
+
+ @Override
+ public IBinder asBinder() {
+ return mWrapper != null ? mWrapper.asBinder() : null;
+ }
+
+ @Override
+ public void binderDied() {
+ if (DEBUG) Log.d(TAG, "binderDeath");
+ handleDeath();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
new file mode 100644
index 000000000000..ca589df5ed3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -0,0 +1,159 @@
+/*
+ * 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.ComponentName;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.service.quicksettings.IQSTileService;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+/**
+ * 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";
+
+ 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;
+
+ TileServiceManager(TileServices tileServices, Handler handler, ComponentName component) {
+ this(tileServices, handler, new TileLifecycleManager(handler,
+ tileServices.getContext(), new Intent().setComponent(component),
+ new UserHandle(ActivityManager.getCurrentUser())));
+ }
+
+ @VisibleForTesting
+ TileServiceManager(TileServices tileServices, Handler handler,
+ TileLifecycleManager tileLifecycleManager) {
+ mServices = tileServices;
+ mHandler = handler;
+ mStateManager = tileLifecycleManager;
+ }
+
+ public IQSTileService getTileService() {
+ return mStateManager;
+ }
+
+ public void setBindRequested(boolean bindRequested) {
+ if (mBindRequested == bindRequested) return;
+ mBindRequested = bindRequested;
+ if (mBindAllowed && mBindRequested && !mBound) {
+ bindService();
+ } else {
+ mServices.recalculateBindAllowance();
+ }
+ if (mBound && !mBindRequested) {
+ // TODO: Schedule unbind.
+ }
+ }
+
+ public void setLastUpdate(long lastUpdate) {
+ mLastUpdate = lastUpdate;
+ mServices.recalculateBindAllowance();
+ }
+
+ public void handleDestroy() {
+ mStateManager.handleDestroy();
+ }
+
+ public void setBindAllowed(boolean allowed) {
+ if (mBindAllowed == allowed) return;
+ mBindAllowed = allowed;
+ if (!mBindAllowed && mBound) {
+ unbindService();
+ } else if (mBindAllowed && mBindRequested && !mBound) {
+ bindService();
+ }
+ }
+
+ private void bindService() {
+ if (mBound) {
+ Log.e(TAG, "Service already bound");
+ return;
+ }
+ 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 (mJustBound) {
+ // If we just bound, lets not thrash on binding/unbinding too much, this is second most
+ // important.
+ mPriority = Integer.MAX_VALUE - 1;
+ } 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 - 2) {
+ mPriority = Integer.MAX_VALUE - 2;
+ } else {
+ mPriority = (int) timeSinceUpdate;
+ }
+ }
+ }
+
+ public int getBindPriority() {
+ return mPriority;
+ }
+
+ @VisibleForTesting
+ final Runnable mJustBoundOver = new Runnable() {
+ @Override
+ public void run() {
+ mJustBound = false;
+ mServices.recalculateBindAllowance();
+ }
+ };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
new file mode 100644
index 000000000000..d110d978293a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java
@@ -0,0 +1,156 @@
+/*
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.service.quicksettings.IQSService;
+import android.service.quicksettings.Tile;
+import android.util.ArrayMap;
+import com.android.systemui.statusbar.phone.QSTileHost;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+
+/**
+ * Runs the day-to-day operations of which tiles should be bound and when.
+ */
+public class TileServices extends IQSService.Stub {
+ static final int DEFAULT_MAX_BOUND = 3;
+ static final int REDUCED_MAX_BOUND = 1;
+
+ private final ArrayMap mServices = new ArrayMap<>();
+ private final ArrayMap mTiles = new ArrayMap<>();
+ private final Context mContext;
+ private final Handler mHandler;
+ private final QSTileHost mHost;
+
+ private int mMaxBound = DEFAULT_MAX_BOUND;
+
+ public TileServices(QSTileHost host, Looper looper) {
+ mHost = host;
+ mContext = mHost.getContext();
+ mHandler = new Handler(looper);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public TileServiceManager getTileWrapper(CustomTile tile) {
+ ComponentName component = tile.getComponent();
+ TileServiceManager service = onCreateTileService(component);
+ synchronized (mServices) {
+ mServices.put(tile, service);
+ mTiles.put(component, tile);
+ }
+ return service;
+ }
+
+ protected TileServiceManager onCreateTileService(ComponentName component) {
+ return new TileServiceManager(this, mHandler, component);
+ }
+
+ public void freeService(CustomTile tile, TileServiceManager service) {
+ synchronized (mServices) {
+ service.setBindAllowed(false);
+ mServices.remove(tile);
+ mTiles.remove(tile.getComponent());
+ }
+ }
+
+ public void setMemoryPressure(boolean memoryPressure) {
+ mMaxBound = memoryPressure ? REDUCED_MAX_BOUND : DEFAULT_MAX_BOUND;
+ recalculateBindAllowance();
+ }
+
+ public void recalculateBindAllowance() {
+ final ArrayList services;
+ synchronized (mServices) {
+ services = new ArrayList<>(mServices.values());
+ }
+ final int N = services.size();
+ if (N > mMaxBound) {
+ long currentTime = System.currentTimeMillis();
+ // Precalculate the priority of services for binding.
+ for (int i = 0; i < N; i++) {
+ services.get(i).calculateBindPriority(currentTime);
+ }
+ // Sort them so we can bind the most important first.
+ Collections.sort(services, SERVICE_SORT);
+ }
+ int i;
+ // Allow mMaxBound items to bind.
+ for (i = 0; i < mMaxBound && i < N; i++) {
+ services.get(i).setBindAllowed(true);
+ }
+ // The rest aren't allowed to bind for now.
+ while (i < N) {
+ services.get(i).setBindAllowed(false);
+ i++;
+ }
+ }
+
+ private void verifyCaller(String packageName) {
+ try {
+ int uid = mContext.getPackageManager().getPackageUid(packageName,
+ Binder.getCallingUserHandle().getIdentifier());
+ if (Binder.getCallingUid() != uid) {
+ throw new SecurityException("Component outside caller's uid");
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ throw new SecurityException(e);
+ }
+ }
+
+ @Override
+ public void updateQsTile(Tile tile) {
+ verifyCaller(tile.getComponentName().getPackageName());
+ CustomTile customTile = getTileForComponent(tile.getComponentName());
+ if (customTile != null) {
+ mServices.get(customTile).setLastUpdate(System.currentTimeMillis());
+ customTile.updateState(tile);
+ customTile.refreshState();
+ }
+ }
+
+ @Override
+ public void onShowDialog(Tile tile) {
+ verifyCaller(tile.getComponentName().getPackageName());
+ CustomTile customTile = getTileForComponent(tile.getComponentName());
+ if (customTile != null) {
+ customTile.onDialogShown();
+ mHost.collapsePanels();
+ }
+ }
+
+ private CustomTile getTileForComponent(ComponentName component) {
+ return mTiles.get(component);
+ }
+
+ private static final Comparator SERVICE_SORT =
+ new Comparator() {
+ @Override
+ public int compare(TileServiceManager left, TileServiceManager right) {
+ return -Integer.compare(left.getBindPriority(), right.getBindPriority());
+ }
+ };
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomTile.java
deleted file mode 100644
index bb74f341b89d..000000000000
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CustomTile.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * 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.tiles;
-
-import android.app.ActivityManager;
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.content.pm.ServiceInfo;
-import android.graphics.drawable.Drawable;
-import android.os.Binder;
-import android.os.IBinder;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.service.quicksettings.IQSTileService;
-import android.service.quicksettings.Tile;
-import android.util.Log;
-import android.view.IWindowManager;
-import android.view.WindowManager;
-import android.view.WindowManagerGlobal;
-import com.android.internal.logging.MetricsLogger;
-import com.android.systemui.qs.QSTile;
-import com.android.systemui.qs.QSTileServiceWrapper;
-import com.android.systemui.statusbar.phone.QSTileHost;
-
-public class CustomTile extends QSTile {
- public static final String PREFIX = "custom(";
-
- private static final boolean DEBUG = false;
-
- // We don't want to thrash binding and unbinding if the user opens and closes the panel a lot.
- // So instead we have a period of waiting.
- private static final long UNBIND_DELAY = 30000;
-
- private final ComponentName mComponent;
- private final Tile mTile;
- private final IWindowManager mWindowManager;
- private final IBinder mToken = new Binder();
-
- private QSTileServiceWrapper mService;
- private boolean mListening;
- private boolean mBound;
- private boolean mIsTokenGranted;
- private boolean mIsShowingDialog;
-
- private CustomTile(QSTileHost host, String action) {
- super(host);
- mWindowManager = WindowManagerGlobal.getWindowManagerService();
- mComponent = ComponentName.unflattenFromString(action);
- mTile = new Tile(mComponent, host);
- try {
- PackageManager pm = mContext.getPackageManager();
- ServiceInfo info = pm.getServiceInfo(mComponent, 0);
- mTile.setIcon(android.graphics.drawable.Icon
- .createWithResource(mComponent.getPackageName(), info.icon));
- mTile.setLabel(info.loadLabel(pm));
- } catch (Exception e) {
- }
- }
-
- public ComponentName getComponent() {
- return mComponent;
- }
-
- public Tile getQsTile() {
- return mTile;
- }
-
- public void updateState(Tile tile) {
- mTile.setIcon(tile.getIcon());
- mTile.setLabel(tile.getLabel());
- mTile.setContentDescription(tile.getContentDescription());
- }
-
- public void onDialogShown() {
- mIsShowingDialog = true;
- }
-
- @Override
- public void setListening(boolean listening) {
- if (mListening == listening) return;
- mListening = listening;
- if (listening) {
- mHandler.removeCallbacks(mUnbind);
- if (!mBound) {
- // TODO: Guarantee re-bind on user-switch.
- mContext.bindServiceAsUser(new Intent().setComponent(mComponent),
- mServiceConnection, Service.BIND_AUTO_CREATE,
- new UserHandle(ActivityManager.getCurrentUser()));
- mBound = true;
- } else {
- if (mService != null) {
- mService.onStartListening();
- } else {
- Log.d(TAG, "Can't start service listening");
- }
- }
- } else {
- if (mService != null) {
- mService.onStopListening();
- }
- if (mIsTokenGranted && !mIsShowingDialog) {
- try {
- if (DEBUG) Log.d(TAG, "Removing token");
- mWindowManager.removeWindowToken(mToken);
- } catch (RemoteException e) {
- }
- mIsTokenGranted = false;
- }
- mIsShowingDialog = false;
- mHandler.postDelayed(mUnbind, UNBIND_DELAY);
- }
- }
-
- @Override
- protected void handleDestroy() {
- super.handleDestroy();
- mHandler.removeCallbacks(mUnbind);
- if (mIsTokenGranted) {
- try {
- if (DEBUG) Log.d(TAG, "Removing token");
- mWindowManager.removeWindowToken(mToken);
- } catch (RemoteException e) {
- }
- }
- mUnbind.run();
- }
-
- @Override
- protected State newTileState() {
- return new State();
- }
-
- @Override
- protected void handleUserSwitch(int newUserId) {
- super.handleUserSwitch(newUserId);
- }
-
- @Override
- protected void handleClick() {
- if (mService != null) {
- try {
- if (DEBUG) Log.d(TAG, "Adding token");
- mWindowManager.addWindowToken(mToken, WindowManager.LayoutParams.TYPE_QS_DIALOG);
- mIsTokenGranted = true;
- } catch (RemoteException e) {
- }
- mService.onClick(mToken);
- } else {
- Log.e(TAG, "Click with no service " + getTileSpec());
- }
- MetricsLogger.action(mContext, getMetricsCategory(), mComponent.getPackageName());
- }
-
- @Override
- protected void handleLongClick() {
- }
-
- @Override
- protected void handleUpdateState(State state, Object arg) {
- Drawable drawable = mTile.getIcon().loadDrawable(mContext);
- drawable.setTint(mContext.getColor(android.R.color.white));
- state.icon = new DrawableIcon(drawable);
- state.label = mTile.getLabel();
- if (mTile.getContentDescription() != null) {
- state.contentDescription = mTile.getContentDescription();
- } else {
- state.contentDescription = state.label;
- }
- }
-
- @Override
- public int getMetricsCategory() {
- return MetricsLogger.QS_INTENT;
- }
-
- private final ServiceConnection mServiceConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- mService = new QSTileServiceWrapper(IQSTileService.Stub.asInterface(service));
- if (mListening) {
- mService.setQSTile(mTile);
- mService.onStartListening();
- } else {
- mService.onStopListening();
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- };
-
- private final Runnable mUnbind = new Runnable() {
- @Override
- public void run() {
- mContext.unbindService(mServiceConnection);
- mBound = false;
- }
- };
-
- public static ComponentName getComponentFromSpec(String spec) {
- final String action = spec.substring(PREFIX.length(), spec.length() - 1);
- if (action.isEmpty()) {
- throw new IllegalArgumentException("Empty custom tile spec action");
- }
- return ComponentName.unflattenFromString(action);
- }
-
- public static QSTile> create(QSTileHost host, String spec) {
- if (spec == null || !spec.startsWith(PREFIX) || !spec.endsWith(")")) {
- throw new IllegalArgumentException("Bad custom tile spec: " + spec);
- }
- final String action = spec.substring(PREFIX.length(), spec.length() - 1);
- if (action.isEmpty()) {
- throw new IllegalArgumentException("Empty custom tile spec action");
- }
- return new CustomTile(host, action);
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
index e6d837a06d4d..90a688ff4364 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
@@ -18,30 +18,25 @@ package com.android.systemui.statusbar.phone;
import android.app.ActivityManager;
import android.app.PendingIntent;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
-import android.os.Binder;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Process;
-import android.os.RemoteException;
import android.provider.Settings;
-import android.service.quicksettings.IQSService;
-import android.service.quicksettings.Tile;
import android.text.TextUtils;
import android.util.Log;
import com.android.systemui.R;
import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.external.CustomTile;
+import com.android.systemui.qs.external.TileServices;
import com.android.systemui.qs.tiles.AirplaneModeTile;
import com.android.systemui.qs.tiles.BatteryTile;
import com.android.systemui.qs.tiles.BluetoothTile;
import com.android.systemui.qs.tiles.CastTile;
import com.android.systemui.qs.tiles.CellularTile;
import com.android.systemui.qs.tiles.ColorInversionTile;
-import com.android.systemui.qs.tiles.CustomTile;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.FlashlightTile;
import com.android.systemui.qs.tiles.HotspotTile;
@@ -76,7 +71,7 @@ import java.util.List;
import java.util.Map;
/** Platform implementation of the quick settings tile host **/
-public final class QSTileHost extends IQSService.Stub implements QSTile.Host, Tunable {
+public final class QSTileHost implements QSTile.Host, Tunable {
private static final String TAG = "QSTileHost";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -100,6 +95,7 @@ public final class QSTileHost extends IQSService.Stub implements QSTile.Host, Tu
private final KeyguardMonitor mKeyguard;
private final SecurityController mSecurity;
private final BatteryController mBattery;
+ private final TileServices mServices;
private final List mCallbacks = new ArrayList<>();
@@ -131,6 +127,8 @@ public final class QSTileHost extends IQSService.Stub implements QSTile.Host, Tu
ht.start();
mLooper = ht.getLooper();
+ mServices = new TileServices(this, mLooper);
+
TunerService.get(mContext).addTunable(this, TILES_SETTING);
}
@@ -256,6 +254,10 @@ public final class QSTileHost extends IQSService.Stub implements QSTile.Host, Tu
return mSecurity;
}
+ public TileServices getTileServices() {
+ return mServices;
+ }
+
@Override
public void onTuningChanged(String key, String newValue) {
if (!TILES_SETTING.equals(key)) {
@@ -306,50 +308,6 @@ public final class QSTileHost extends IQSService.Stub implements QSTile.Host, Tu
TextUtils.join(",", specs), ActivityManager.getCurrentUser());
}
- @Override
- public void updateQsTile(Tile tile) throws RemoteException {
- verifyCaller(tile.getComponentName().getPackageName());
- CustomTile customTile = getTileForComponent(tile.getComponentName());
- if (customTile != null) {
- customTile.updateState(tile);
- customTile.refreshState();
- }
- }
-
- @Override
- public void onShowDialog(Tile tile) throws RemoteException {
- verifyCaller(tile.getComponentName().getPackageName());
- CustomTile customTile = getTileForComponent(tile.getComponentName());
- if (customTile != null) {
- customTile.onDialogShown();
- collapsePanels();
- }
- }
-
- private void verifyCaller(String packageName) {
- try {
- int uid = mContext.getPackageManager().getPackageUid(packageName,
- Binder.getCallingUserHandle().getIdentifier());
- if (Binder.getCallingUid() != uid) {
- throw new SecurityException("Component outside caller's uid");
- }
- } catch (NameNotFoundException e) {
- throw new SecurityException(e);
- }
- }
-
- private CustomTile getTileForComponent(ComponentName component) {
- // TODO: Build map for easier lookup.
- for (QSTile> qsTile : mTiles.values()) {
- if (qsTile instanceof CustomTile) {
- if (((CustomTile) qsTile).getComponent().equals(component)) {
- return (CustomTile) qsTile;
- }
- }
- }
- return null;
- }
-
public QSTile> createTile(String tileSpec) {
if (tileSpec.equals("wifi")) return WifiTile.isSupported(this)
? new WifiTile(this) : null;
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
index 1e3b0f1d7b8c..9081af156f00 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerService.java
@@ -153,8 +153,11 @@ public class TunerService extends SystemUI {
private static TunerService sInstance;
public static TunerService get(Context context) {
- SystemUIApplication sysUi = (SystemUIApplication) context.getApplicationContext();
- TunerService service = sysUi.getComponent(TunerService.class);
+ TunerService service = null;
+ if (context.getApplicationContext() instanceof SystemUIApplication) {
+ SystemUIApplication sysUi = (SystemUIApplication) context.getApplicationContext();
+ service = sysUi.getComponent(TunerService.class);
+ }
if (service == null) {
// Can't get it as a component, must in the tuner, lets just create one for now.
return getStaticService(context);
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index c21af2457030..282560100cb4 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -19,10 +19,16 @@
+
+
+
+
mCallbacks = new ArraySet<>();
+ private boolean mBound;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mThread = new HandlerThread("TestThread");
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
+ mStateManager = new TileLifecycleManager(mHandler, getContext(),
+ new Intent(mContext, FakeTileService.class), new UserHandle(UserHandle.myUserId()));
+ mCallbacks.clear();
+ getContext().registerReceiver(mReceiver, new IntentFilter(TILE_UPDATE_BROADCAST));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ if (mBound) {
+ unbindService();
+ }
+ mThread.quit();
+ getContext().unregisterReceiver(mReceiver);
+ }
+
+ public void testSync() {
+ syncWithHandler();
+ }
+
+ public void testBind() {
+ bindService();
+ waitForCallback("onCreate");
+ }
+
+ public void testUnbind() {
+ bindService();
+ waitForCallback("onCreate");
+ unbindService();
+ waitForCallback("onDestroy");
+ }
+
+ public void testTileServiceCallbacks() {
+ bindService();
+ waitForCallback("onCreate");
+
+ mStateManager.onTileAdded();
+ waitForCallback("onTileAdded");
+ mStateManager.onStartListening();
+ waitForCallback("onStartListening");
+ mStateManager.onClick(null);
+ waitForCallback("onClick");
+ mStateManager.onStopListening();
+ waitForCallback("onStopListening");
+ mStateManager.onTileRemoved();
+ waitForCallback("onTileRemoved");
+
+ unbindService();
+ }
+
+ public void testAddedBeforeBind() {
+ mStateManager.onTileAdded();
+
+ bindService();
+ waitForCallback("onCreate");
+ waitForCallback("onTileAdded");
+ }
+
+ public void testListeningBeforeBind() {
+ mStateManager.onTileAdded();
+ mStateManager.onStartListening();
+
+ bindService();
+ waitForCallback("onCreate");
+ waitForCallback("onTileAdded");
+ waitForCallback("onStartListening");
+ }
+
+ public void testClickBeforeBind() {
+ mStateManager.onTileAdded();
+ mStateManager.onStartListening();
+ mStateManager.onClick(null);
+
+ bindService();
+ waitForCallback("onCreate");
+ waitForCallback("onTileAdded");
+ waitForCallback("onStartListening");
+ waitForCallback("onClick");
+ }
+
+ public void testListeningNotListeningBeforeBind() {
+ mStateManager.onTileAdded();
+ mStateManager.onStartListening();
+ mStateManager.onStopListening();
+
+ bindService();
+ waitForCallback("onCreate");
+ unbindService();
+ waitForCallback("onDestroy");
+ assertFalse(mCallbacks.contains("onStartListening"));
+ }
+
+ public void testNoClickOfNotListeningAnymore() {
+ mStateManager.onTileAdded();
+ mStateManager.onStartListening();
+ mStateManager.onClick(null);
+ mStateManager.onStopListening();
+
+ bindService();
+ waitForCallback("onCreate");
+ unbindService();
+ waitForCallback("onDestroy");
+ assertFalse(mCallbacks.contains("onClick"));
+ }
+
+ public void testComponentEnabling() {
+ mStateManager.onTileAdded();
+ mStateManager.onStartListening();
+
+ PackageManager pm = getContext().getPackageManager();
+ pm.setComponentEnabledSetting(new ComponentName(getContext(), FakeTileService.class),
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
+
+ bindService();
+ assertTrue(mStateManager.mReceiverRegistered);
+
+ pm.setComponentEnabledSetting(new ComponentName(getContext(), FakeTileService.class),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+ waitForCallback("onCreate");
+ }
+
+ public void testKillProcess() {
+ mStateManager.onStartListening();
+ bindService();
+ waitForCallback("onCreate");
+ waitForCallback("onStartListening");
+
+ getContext().sendBroadcast(new Intent(FakeTileService.ACTION_KILL));
+
+ waitForCallback("onCreate");
+ waitForCallback("onStartListening");
+ }
+
+ private void bindService() {
+ mBound = true;
+ mStateManager.setBindService(true);
+ }
+
+ private void unbindService() {
+ mBound = false;
+ mStateManager.setBindService(false);
+ }
+
+ private void waitForCallback(String callback) {
+ for (int i = 0; i < 25; i++) {
+ if (mCallbacks.contains(callback)) {
+ mCallbacks.remove(callback);
+ return;
+ }
+ synchronized (mBroadcastLock) {
+ try {
+ mBroadcastLock.wait(500);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+ if (mCallbacks.contains(callback)) {
+ mCallbacks.remove(callback);
+ return;
+ }
+ fail("Didn't receive callback: " + callback);
+ }
+
+ private void syncWithHandler() {
+ final Object lock = new Object();
+ synchronized (lock) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ synchronized (lock) {
+ lock.notify();
+ }
+ }
+ });
+ try {
+ lock.wait(5000);
+ } catch (InterruptedException e) {
+ }
+ }
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mCallbacks.add(intent.getStringExtra(EXTRA_CALLBACK));
+ synchronized (mBroadcastLock) {
+ mBroadcastLock.notify();
+ }
+ }
+ };
+
+ public static class FakeTileService extends TileService {
+ public static final String ACTION_KILL = "com.android.systemui.test.KILL";
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ registerReceiver(mReceiver, new IntentFilter(ACTION_KILL));
+ sendCallback("onCreate");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mReceiver);
+ sendCallback("onDestroy");
+ }
+
+ @Override
+ public void onTileAdded() {
+ sendCallback("onTileAdded");
+ }
+
+ @Override
+ public void onTileRemoved() {
+ sendCallback("onTileRemoved");
+ }
+
+ @Override
+ public void onStartListening() {
+ sendCallback("onStartListening");
+ }
+
+ @Override
+ public void onStopListening() {
+ sendCallback("onStopListening");
+ }
+
+ @Override
+ public void onClick() {
+ sendCallback("onClick");
+ }
+
+ private void sendCallback(String callback) {
+ Log.d("TileLifecycleManager", "Relaying: " + callback);
+ sendBroadcast(new Intent(TILE_UPDATE_BROADCAST)
+ .putExtra(EXTRA_CALLBACK, callback));
+ }
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_KILL.equals(intent.getAction())) {
+ Process.killProcess(Process.myPid());
+ }
+ }
+ };
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTests.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTests.java
new file mode 100644
index 000000000000..c4f686e98cf5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTests.java
@@ -0,0 +1,93 @@
+/*
+ * 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.os.Handler;
+import android.os.HandlerThread;
+import com.android.systemui.SysuiTestCase;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+public class TileServiceManagerTests extends SysuiTestCase {
+
+ private TileServices mTileServices;
+ private TileLifecycleManager mTileLifecycle;
+ private HandlerThread mThread;
+ private Handler mHandler;
+ private TileServiceManager mTileServiceManager;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mThread = new HandlerThread("TestThread");
+ mThread.start();
+ mHandler = new Handler(mThread.getLooper());
+ mTileServices = Mockito.mock(TileServices.class);
+ mTileLifecycle = Mockito.mock(TileLifecycleManager.class);
+ mTileServiceManager = new TileServiceManager(mTileServices, mHandler, mTileLifecycle);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mThread.quit();
+ }
+
+ public void testSetBindRequested() {
+ // Request binding.
+ mTileServiceManager.setBindRequested(true);
+ mTileServiceManager.setLastUpdate(0);
+ mTileServiceManager.calculateBindPriority(5);
+ Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance();
+ assertEquals(5, mTileServiceManager.getBindPriority());
+
+ // Verify same state doesn't trigger recalculating for no reason.
+ mTileServiceManager.setBindRequested(true);
+ Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance();
+
+ mTileServiceManager.setBindRequested(false);
+ mTileServiceManager.calculateBindPriority(5);
+ Mockito.verify(mTileServices, Mockito.times(3)).recalculateBindAllowance();
+ assertEquals(Integer.MIN_VALUE, mTileServiceManager.getBindPriority());
+ }
+
+ public void testPendingClickPriority() {
+ Mockito.when(mTileLifecycle.hasPendingClick()).thenReturn(true);
+ mTileServiceManager.calculateBindPriority(0);
+ assertEquals(Integer.MAX_VALUE, mTileServiceManager.getBindPriority());
+ }
+
+ public void testBind() {
+ // Trigger binding requested and allowed.
+ mTileServiceManager.setBindRequested(true);
+ mTileServiceManager.setBindAllowed(true);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+ Mockito.verify(mTileLifecycle, Mockito.times(1)).setBindService(captor.capture());
+ assertTrue((boolean) captor.getValue());
+
+ mTileServiceManager.setBindRequested(false);
+ mTileServiceManager.calculateBindPriority(0);
+ // Priority shouldn't disappear after the request goes away if we just bound, instead
+ // it sticks around to avoid thrashing a bunch of processes.
+ assertEquals(Integer.MAX_VALUE - 1, mTileServiceManager.getBindPriority());
+
+ mTileServiceManager.setBindAllowed(false);
+ captor = ArgumentCaptor.forClass(Boolean.class);
+ Mockito.verify(mTileLifecycle, Mockito.times(2)).setBindService(captor.capture());
+ assertFalse((boolean) captor.getValue());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTests.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTests.java
new file mode 100644
index 000000000000..7a3ce878b8cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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.content.ComponentName;
+import android.os.Looper;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+
+public class TileServicesTests extends SysuiTestCase {
+ private static int NUM_FAKES = TileServices.DEFAULT_MAX_BOUND * 2;
+
+ private TileServices mTileService;
+ private ArrayList mManagers;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mManagers = new ArrayList<>();
+ QSTileHost host = new QSTileHost(mContext, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null);
+ mTileService = new TestTileServices(host, Looper.myLooper());
+ }
+
+ public void testRecalculateBindAllowance() {
+ // Add some fake tiles.
+ for (int i = 0; i < NUM_FAKES; i++) {
+ mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+ }
+ assertEquals(NUM_FAKES, mManagers.size());
+
+ for (int i = 0; i < NUM_FAKES; i++) {
+ Mockito.when(mManagers.get(i).getBindPriority()).thenReturn(i);
+ }
+ mTileService.recalculateBindAllowance();
+ for (int i = 0; i < NUM_FAKES; i++) {
+ Mockito.verify(mManagers.get(i), Mockito.times(1)).calculateBindPriority(
+ Mockito.anyLong());
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+ Mockito.verify(mManagers.get(i), Mockito.times(1)).setBindAllowed(captor.capture());
+
+ assertEquals("" + i + "th service", i >= (NUM_FAKES - TileServices.DEFAULT_MAX_BOUND),
+ (boolean) captor.getValue());
+ }
+ }
+
+ public void testSetMemoryPressure() {
+ testRecalculateBindAllowance();
+ mTileService.setMemoryPressure(true);
+
+ for (int i = 0; i < NUM_FAKES; i++) {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+ Mockito.verify(mManagers.get(i), Mockito.times(2)).setBindAllowed(captor.capture());
+
+ assertEquals("" + i + "th service", i >= (NUM_FAKES - TileServices.REDUCED_MAX_BOUND),
+ (boolean) captor.getValue());
+ }
+ }
+
+ public void testCalcFew() {
+ for (int i = 0; i < TileServices.DEFAULT_MAX_BOUND - 1; i++) {
+ mTileService.getTileWrapper(Mockito.mock(CustomTile.class));
+ }
+ mTileService.recalculateBindAllowance();
+
+ for (int i = 0; i < TileServices.DEFAULT_MAX_BOUND - 1; i++) {
+ // Shouldn't get bind prioirities calculated when there are less than the max services.
+ Mockito.verify(mManagers.get(i), Mockito.never()).calculateBindPriority(
+ Mockito.anyLong());
+
+ // All should be bound since there are less than the max services.
+ ArgumentCaptor captor = ArgumentCaptor.forClass(Boolean.class);
+ Mockito.verify(mManagers.get(i), Mockito.times(1)).setBindAllowed(captor.capture());
+
+ assertTrue(captor.getValue());
+ }
+ }
+
+ private class TestTileServices extends TileServices {
+ public TestTileServices(QSTileHost host, Looper looper) {
+ super(host, looper);
+ }
+
+ @Override
+ protected TileServiceManager onCreateTileService(ComponentName component) {
+ TileServiceManager manager = Mockito.mock(TileServiceManager.class);
+ mManagers.add(manager);
+ return manager;
+ }
+ }
+}
--
cgit v1.2.3-59-g8ed1b