Loading packages/SystemUI/src/com/android/systemui/qs/QSTile.java +3 −4 Original line number Diff line number Diff line Loading @@ -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<>(); Loading Loading @@ -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)); } /** Loading Loading @@ -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); } Loading @@ -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); } Loading packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +16 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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( Loading @@ -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); Loading Loading @@ -204,8 +216,10 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene } private void save() { if (mFinishedFetchingTiles) { mTileAdapter.saveSpecs(mHost); } } private final Callback mKeyguardCallback = () -> { if (Dependency.get(KeyguardMonitor.class).isShowing()) { Loading packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +67 −76 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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()) { Loading @@ -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(); Loading @@ -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); Loading @@ -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; Loading @@ -182,7 +147,8 @@ 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) { Loading @@ -196,11 +162,36 @@ public class TileQueryHelper { 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 { Loading packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java 0 → 100644 +87 −0 Original line number Diff line number Diff line /* * 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); } } Loading
packages/SystemUI/src/com/android/systemui/qs/QSTile.java +3 −4 Original line number Diff line number Diff line Loading @@ -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<>(); Loading Loading @@ -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)); } /** Loading Loading @@ -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); } Loading @@ -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); } Loading
packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +16 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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( Loading @@ -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); Loading Loading @@ -204,8 +216,10 @@ public class QSCustomizer extends LinearLayout implements OnMenuItemClickListene } private void save() { if (mFinishedFetchingTiles) { mTileAdapter.saveSpecs(mHost); } } private final Callback mKeyguardCallback = () -> { if (Dependency.get(KeyguardMonitor.class).isShowing()) { Loading
packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +67 −76 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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()) { Loading @@ -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(); Loading @@ -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); Loading @@ -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; Loading @@ -182,7 +147,8 @@ 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) { Loading @@ -196,11 +162,36 @@ public class TileQueryHelper { 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 { Loading
packages/SystemUI/tests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java 0 → 100644 +87 −0 Original line number Diff line number Diff line /* * 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); } }