From 6b28473635e5c9550b7a57c7ec23e60ed9185b2b Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Tue, 28 Feb 2017 17:27:04 -0500 Subject: [PATCH] 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 --- .../src/com/android/systemui/qs/QSTile.java | 7 +- .../systemui/qs/customize/QSCustomizer.java | 18 ++- .../qs/customize/TileQueryHelper.java | 153 +++++++++--------- .../qs/customize/TileQueryHelperTest.java | 87 ++++++++++ 4 files changed, 178 insertions(+), 87 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index c02067e698e11..eb748af0c80cd 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 { 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 mListeners = new ArraySet<>(); @@ -86,7 +86,6 @@ public abstract class QSTile { 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 { 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 { 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 730b55d020172..98ec6a57edee3 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 72e6fcc0c35f0..386294a945ee0 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 mTiles = new ArrayList<>(); private final ArrayList 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,21 +106,60 @@ public class TileQueryHelper { } }); } - qsHandler.post(new Runnable() { - @Override - public void run() { - mainHandler.post(new Runnable() { - @Override - public void run() { - new QueryTilesTask().execute(host.getTiles()); - } - }); + } + + private void addPackageTiles(Handler mainHandler, Handler bgHandler) { + bgHandler.post(() -> { + Collection> params = mHost.getTiles(); + PackageManager pm = mContext.getPackageManager(); + List 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); + + // Don't include apps that are a part of the default tile set. + if (stockTiles.contains(componentName.flattenToString())) { + continue; + } + + final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm); + String spec = CustomTile.toSpec(componentName); + State state = getState(params, spec); + if (state != null) { + addTile(spec, appLabel, state, false); + continue; + } + if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) { + continue; + } + Drawable icon = info.serviceInfo.loadIcon(pm); + if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) { + continue; + } + if (icon == null) { + continue; + } + icon.mutate(); + icon.setTint(mContext.getColor(android.R.color.white)); + CharSequence label = info.serviceInfo.loadLabel(pm); + addTile(spec, icon, label != null ? label.toString() : "null", appLabel, mContext); } + mainHandler.post(() -> mListener.onTilesChanged(mTiles)); }); } - public void setListener(TileStateListener listener) { - mListener = listener; + private State getState(Collection> 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; } private void addTile(String spec, CharSequence appLabel, State state, boolean isSystem) { @@ -142,67 +194,6 @@ public class TileQueryHelper { public boolean isSystem; } - private class QueryTilesTask extends - AsyncTask>, Void, Collection> { - @Override - protected Collection doInBackground(Collection>... params) { - List tiles = new ArrayList<>(); - PackageManager pm = mContext.getPackageManager(); - List 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); - - // Don't include apps that are a part of the default tile set. - if (stockTiles.contains(componentName.flattenToString())) { - continue; - } - - final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm); - String spec = CustomTile.toSpec(componentName); - State state = getState(params[0], spec); - if (state != null) { - addTile(spec, appLabel, state, false); - continue; - } - if (info.serviceInfo.icon == 0 && info.serviceInfo.applicationInfo.icon == 0) { - continue; - } - Drawable icon = info.serviceInfo.loadIcon(pm); - if (!permission.BIND_QUICK_SETTINGS_TILE.equals(info.serviceInfo.permission)) { - continue; - } - if (icon == null) { - continue; - } - icon.mutate(); - icon.setTint(mContext.getColor(android.R.color.white)); - CharSequence label = info.serviceInfo.loadLabel(pm); - addTile(spec, icon, label != null ? label.toString() : "null", appLabel, mContext); - } - return tiles; - } - - private State getState(Collection> 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; - } - - @Override - protected void onPostExecute(Collection result) { - mTiles.addAll(result); - mListener.onTilesChanged(mTiles); - } - } - public interface TileStateListener { void onTilesChanged(List tiles); } 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 0000000000000..b9838204e1a33 --- /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); + } +}