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); + } +}