From d5a204f16e7c71ffdbc6c8307a4134dcc1efd60d Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Mon, 21 Dec 2015 08:50:01 -0500 Subject: Better service management for QS 3rd party tiles Better wrapper around the service that will handle rebinding when it dies, binding when it becomes available, and forwarding along all messages to the service once it binds. Also better handling of too many services at a time. Will only ever bind to at most 3 tiles and will manage which ones are most important to show based on pending clicks, last update, etc. Change-Id: I5f4da0bc751f7eb25baa32e5c0bb9f1bc418f5bb --- .../android/service/quicksettings/TileService.java | 12 +- .../android/internal/logging/MetricsLogger.java | 1 + core/res/AndroidManifest.xml | 2 +- .../android/systemui/qs/QSTileServiceWrapper.java | 76 ----- .../systemui/qs/customize/CustomQSPanel.java | 4 +- .../android/systemui/qs/customize/TileAdapter.java | 2 +- .../android/systemui/qs/external/CustomTile.java | 200 +++++++++++++ .../systemui/qs/external/QSTileServiceWrapper.java | 96 ++++++ .../systemui/qs/external/TileLifecycleManager.java | 331 +++++++++++++++++++++ .../systemui/qs/external/TileServiceManager.java | 159 ++++++++++ .../android/systemui/qs/external/TileServices.java | 156 ++++++++++ .../com/android/systemui/qs/tiles/CustomTile.java | 236 --------------- .../systemui/statusbar/phone/QSTileHost.java | 62 +--- .../com/android/systemui/tuner/TunerService.java | 7 +- packages/SystemUI/tests/AndroidManifest.xml | 6 + .../qs/external/TileLifecycleManagerTests.java | 298 +++++++++++++++++++ .../qs/external/TileServiceManagerTests.java | 93 ++++++ .../systemui/qs/external/TileServicesTests.java | 108 +++++++ 18 files changed, 1473 insertions(+), 376 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/QSTileServiceWrapper.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/CustomTile.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/QSTileServiceWrapper.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java create mode 100644 packages/SystemUI/src/com/android/systemui/qs/external/TileServices.java delete mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/CustomTile.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTests.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTests.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTests.java diff --git a/core/java/android/service/quicksettings/TileService.java b/core/java/android/service/quicksettings/TileService.java index d8787b4caca7..55fe4cd901e1 100644 --- a/core/java/android/service/quicksettings/TileService.java +++ b/core/java/android/service/quicksettings/TileService.java @@ -26,17 +26,17 @@ import android.os.RemoteException; import android.view.WindowManager; /** - * A QSTileService provides the user a tile that can be added to Quick Settings. + * A TileService provides the user a tile that can be added to Quick Settings. * Quick Settings is a space provided that allows the user to change settings and * take quick actions without leaving the context of their current app. * - *

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.

+ * icon for the tile. Here is an example TileService 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