summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Evan Laird <evanlaird@google.com> 2017-02-28 17:27:04 -0500
committer Evan Laird <evanlaird@google.com> 2017-03-03 10:34:36 -0500
commit6b28473635e5c9550b7a57c7ec23e60ed9185b2b (patch)
tree0ccd1863578361342bfe3646f3a3d53f8b18ffe6
parent596ec855c369b652194f20c198cc8e00d9aadfe0 (diff)
Fix qs tiles disappearing when leaving edit
On presenting the customizer view to edit quick settings tiles, the tiles were fetched in background threads (2 different ones). If the user then managed to dismiss the view too quickly, a save would occur before all tiles were loaded and they would disappear. This change coerces tiles to load on the same background thread and allows the customizer view to know when that operation is complete. Thus, it can save only when it knows that all possible tiles were loaded. Fixes:35556395 Test: runtest -x SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java Change-Id: Ie232d3c28645d38aad97a8763b7418e6b044b5cc
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSTile.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java143
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java87
4 files changed, 173 insertions, 82 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index c02067e698e1..eb748af0c80c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -58,7 +58,7 @@ public abstract class QSTile<TState extends State> {
protected final Host mHost;
protected final Context mContext;
- protected final H mHandler;
+ protected final H mHandler = new H(Dependency.get(Dependency.BG_LOOPER));
protected final Handler mUiHandler = new Handler(Looper.getMainLooper());
private final ArraySet<Object> mListeners = new ArraySet<>();
@@ -86,7 +86,6 @@ public abstract class QSTile<TState extends State> {
protected QSTile(Host host) {
mHost = host;
mContext = host.getContext();
- mHandler = new H(Dependency.get(Dependency.BG_LOOPER));
}
/**
@@ -170,7 +169,7 @@ public abstract class QSTile<TState extends State> {
mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0).sendToTarget();
}
- public final void refreshState() {
+ public void refreshState() {
refreshState(null);
}
@@ -178,7 +177,7 @@ public abstract class QSTile<TState extends State> {
mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
}
- public final void clearState() {
+ public void clearState() {
mHandler.sendEmptyMessage(H.CLEAR_STATE);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
index 730b55d02017..98ec6a57edee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java
@@ -20,6 +20,8 @@ import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Configuration;
+import android.os.Handler;
+import android.os.Looper;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
@@ -69,6 +71,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
private boolean mCustomizing;
private NotificationsQuickSettingsContainer mNotifQsContainer;
private QS mQs;
+ private boolean mFinishedFetchingTiles = false;
public QSCustomizer(Context context, AttributeSet attrs) {
super(new ContextThemeWrapper(context, R.style.edit_theme), attrs);
@@ -136,7 +139,7 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
setTileSpecs();
setVisibility(View.VISIBLE);
mClipper.animateCircularClip(x, y, true, mExpandAnimationListener);
- new TileQueryHelper(mContext, mHost).setListener(mTileAdapter);
+ queryTiles();
mNotifQsContainer.setCustomizerAnimating(true);
mNotifQsContainer.setCustomizerShowing(true);
announceForAccessibility(mContext.getString(
@@ -145,6 +148,15 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
}
}
+ private void queryTiles() {
+ mFinishedFetchingTiles = false;
+ Runnable tileQueryFetchCompletion = () -> {
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ mainHandler.post(() -> mFinishedFetchingTiles = true);
+ };
+ new TileQueryHelper(mContext, mHost, mTileAdapter, tileQueryFetchCompletion);
+ }
+
public void hide(int x, int y) {
if (isShown) {
MetricsLogger.hidden(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
@@ -204,7 +216,9 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene
}
private void save() {
- mTileAdapter.saveSpecs(mHost);
+ if (mFinishedFetchingTiles) {
+ mTileAdapter.saveSpecs(mHost);
+ }
}
private final Callback mKeyguardCallback = () -> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 72e6fcc0c35f..386294a945ee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -24,7 +24,6 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.service.quicksettings.TileService;
@@ -49,22 +48,36 @@ public class TileQueryHelper {
private final ArrayList<TileInfo> mTiles = new ArrayList<>();
private final ArrayList<String> mSpecs = new ArrayList<>();
private final Context mContext;
- private TileStateListener mListener;
+ private final TileStateListener mListener;
+ private final QSTileHost mHost;
+ private final Runnable mCompletion;
- public TileQueryHelper(Context context, QSTileHost host) {
+ public TileQueryHelper(Context context, QSTileHost host,
+ TileStateListener listener, Runnable completion) {
mContext = context;
- addSystemTiles(host);
+ mListener = listener;
+ mHost = host;
+ mCompletion = completion;
+ addSystemTiles();
// TODO: Live?
}
- private void addSystemTiles(final QSTileHost host) {
- String possible = mContext.getString(R.string.quick_settings_tiles_stock);
- String[] possibleTiles = possible.split(",");
+ private void addSystemTiles() {
+ // Enqueue jobs to fetch every system tile and then ever package tile.
final Handler qsHandler = new Handler((Looper) Dependency.get(Dependency.BG_LOOPER));
final Handler mainHandler = new Handler(Looper.getMainLooper());
+ addStockTiles(mainHandler, qsHandler);
+ addPackageTiles(mainHandler, qsHandler);
+ // Then enqueue the completion. It should always be last
+ qsHandler.post(mCompletion);
+ }
+
+ private void addStockTiles(Handler mainHandler, Handler bgHandler) {
+ String possible = mContext.getString(R.string.quick_settings_tiles_stock);
+ String[] possibleTiles = possible.split(",");
for (int i = 0; i < possibleTiles.length; i++) {
final String spec = possibleTiles[i];
- final QSTile<?> tile = host.createTile(spec);
+ final QSTile<?> tile = mHost.createTile(spec);
if (tile == null) {
continue;
} else if (!tile.isAvailable()) {
@@ -75,7 +88,7 @@ public class TileQueryHelper {
tile.clearState();
tile.refreshState();
tile.setListening(this, false);
- qsHandler.post(new Runnable() {
+ bgHandler.post(new Runnable() {
@Override
public void run() {
final QSTile.State state = tile.newTileState();
@@ -93,64 +106,16 @@ public class TileQueryHelper {
}
});
}
- qsHandler.post(new Runnable() {
- @Override
- public void run() {
- mainHandler.post(new Runnable() {
- @Override
- public void run() {
- new QueryTilesTask().execute(host.getTiles());
- }
- });
- }
- });
}
- public void setListener(TileStateListener listener) {
- mListener = listener;
- }
-
- private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) {
- if (mSpecs.contains(spec)) {
- return;
- }
- TileInfo info = new TileInfo();
- info.state = state;
- info.state.minimalAccessibilityClassName = info.state.expandedAccessibilityClassName =
- Button.class.getName();
- info.spec = spec;
- info.appLabel = appLabel;
- info.isSystem = isSystem;
- mTiles.add(info);
- mSpecs.add(spec);
- }
-
- private void addTile(String spec, Drawable drawable, CharSequence label, CharSequence appLabel,
- Context context) {
- QSTile.State state = new QSTile.State();
- state.label = label;
- state.contentDescription = label;
- state.icon = new DrawableIcon(drawable);
- state.autoMirrorDrawable = false;
- addTile(spec, appLabel, state, false);
- }
-
- public static class TileInfo {
- public String spec;
- public CharSequence appLabel;
- public QSTile.State state;
- public boolean isSystem;
- }
-
- private class QueryTilesTask extends
- AsyncTask<Collection<QSTile<?>>, Void, Collection<TileInfo>> {
- @Override
- protected Collection<TileInfo> doInBackground(Collection<QSTile<?>>... params) {
- List<TileInfo> tiles = new ArrayList<>();
+ private void addPackageTiles(Handler mainHandler, Handler bgHandler) {
+ bgHandler.post(() -> {
+ Collection<QSTile<?>> params = mHost.getTiles();
PackageManager pm = mContext.getPackageManager();
List<ResolveInfo> services = pm.queryIntentServicesAsUser(
new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
String stockTiles = mContext.getString(R.string.quick_settings_tiles_stock);
+
for (ResolveInfo info : services) {
String packageName = info.serviceInfo.packageName;
ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
@@ -162,7 +127,7 @@ public class TileQueryHelper {
final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
String spec = CustomTile.toSpec(componentName);
- State state = getState(params[0], spec);
+ State state = getState(params, spec);
if (state != null) {
addTile(spec, appLabel, state, false);
continue;
@@ -182,25 +147,51 @@ public class TileQueryHelper {
CharSequence label = info.serviceInfo.loadLabel(pm);
addTile(spec, icon, label != null ? label.toString() : "null", appLabel, mContext);
}
- return tiles;
- }
+ mainHandler.post(() -> mListener.onTilesChanged(mTiles));
+ });
+ }
- private State getState(Collection<QSTile<?>> tiles, String spec) {
- for (QSTile<?> tile : tiles) {
- if (spec.equals(tile.getTileSpec())) {
- final QSTile.State state = tile.newTileState();
- tile.getState().copyTo(state);
- return state;
- }
+ private State getState(Collection<QSTile<?>> tiles, String spec) {
+ for (QSTile<?> tile : tiles) {
+ if (spec.equals(tile.getTileSpec())) {
+ final QSTile.State state = tile.newTileState();
+ tile.getState().copyTo(state);
+ return state;
}
- return null;
}
+ return null;
+ }
- @Override
- protected void onPostExecute(Collection<TileInfo> result) {
- mTiles.addAll(result);
- mListener.onTilesChanged(mTiles);
+ private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) {
+ if (mSpecs.contains(spec)) {
+ return;
}
+ TileInfo info = new TileInfo();
+ info.state = state;
+ info.state.minimalAccessibilityClassName = info.state.expandedAccessibilityClassName =
+ Button.class.getName();
+ info.spec = spec;
+ info.appLabel = appLabel;
+ info.isSystem = isSystem;
+ mTiles.add(info);
+ mSpecs.add(spec);
+ }
+
+ private void addTile(String spec, Drawable drawable, CharSequence label, CharSequence appLabel,
+ Context context) {
+ QSTile.State state = new QSTile.State();
+ state.label = label;
+ state.contentDescription = label;
+ state.icon = new DrawableIcon(drawable);
+ state.autoMirrorDrawable = false;
+ addTile(spec, appLabel, state, false);
+ }
+
+ public static class TileInfo {
+ public String spec;
+ public CharSequence appLabel;
+ public QSTile.State state;
+ public boolean isSystem;
}
public interface TileStateListener {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
new file mode 100644
index 000000000000..b9838204e1a3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 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.customize;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Message;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.SysUIRunner;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.QSTile.State;
+import com.android.systemui.statusbar.phone.QSTileHost;
+
+import com.android.systemui.utils.TestableLooper;
+import com.android.systemui.utils.TestableLooper.MessageHandler;
+import com.android.systemui.utils.TestableLooper.RunWithLooper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(SysUIRunner.class)
+@RunWithLooper
+public class TileQueryHelperTest extends SysuiTestCase {
+ private TestableLooper mBGLooper;
+ private Runnable mLastCallback;
+
+ @Before
+ public void setup() {
+ mBGLooper = TestableLooper.get(this);
+ injectTestDependency(Dependency.BG_LOOPER, mBGLooper.getLooper());
+ }
+
+ @Test
+ public void testCompletionCalled() {
+ QSTileHost mockHost = mock(QSTileHost.class);
+ TileAdapter mockAdapter = mock(TileAdapter.class);
+ Runnable mockCompletion = mock(Runnable.class);
+ new TileQueryHelper(mContext, mockHost, mockAdapter, mockCompletion);
+ mBGLooper.processAllMessages();
+ verify(mockCompletion).run();
+ }
+
+ @Test
+ public void testCompletionCalledAfterTilesFetched() {
+ QSTile mockTile = mock(QSTile.class);
+ State mockState = mock(State.class);
+ when(mockTile.newTileState()).thenReturn(mockState);
+ when(mockTile.getState()).thenReturn(mockState);
+ when(mockTile.isAvailable()).thenReturn(true);
+
+ QSTileHost mockHost = mock(QSTileHost.class);
+ when(mockHost.createTile(any())).thenReturn(mockTile);
+
+ mBGLooper.setMessageHandler((Message m) -> {
+ mLastCallback = m.getCallback();
+ return true;
+ });
+ TileAdapter mockAdapter = mock(TileAdapter.class);
+ Runnable mockCompletion = mock(Runnable.class);
+ new TileQueryHelper(mContext, mockHost, mockAdapter, mockCompletion);
+
+ // Verify that the last thing in the queue was our callback
+ mBGLooper.processAllMessages();
+ assertEquals(mockCompletion, mLastCallback);
+ }
+}