1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs.customize; 16 17 import static junit.framework.Assert.assertEquals; 18 import static junit.framework.Assert.assertTrue; 19 20 import static org.hamcrest.Matchers.equalTo; 21 import static org.hamcrest.Matchers.is; 22 import static org.junit.Assert.assertFalse; 23 import static org.junit.Assert.assertThat; 24 import static org.mockito.ArgumentMatchers.anyInt; 25 import static org.mockito.ArgumentMatchers.anyString; 26 import static org.mockito.Mockito.any; 27 import static org.mockito.Mockito.atLeastOnce; 28 import static org.mockito.Mockito.doAnswer; 29 import static org.mockito.Mockito.inOrder; 30 import static org.mockito.Mockito.mock; 31 import static org.mockito.Mockito.never; 32 import static org.mockito.Mockito.times; 33 import static org.mockito.Mockito.verify; 34 import static org.mockito.Mockito.when; 35 36 import android.Manifest; 37 import android.content.Context; 38 import android.content.pm.ApplicationInfo; 39 import android.content.pm.PackageManager; 40 import android.content.pm.ResolveInfo; 41 import android.content.pm.ServiceInfo; 42 import android.provider.Settings; 43 import android.service.quicksettings.Tile; 44 import android.testing.AndroidTestingRunner; 45 import android.testing.TestableLooper; 46 import android.text.TextUtils; 47 import android.util.ArraySet; 48 import android.view.View; 49 50 import androidx.annotation.Nullable; 51 import androidx.test.filters.SmallTest; 52 53 import com.android.internal.logging.InstanceId; 54 import com.android.systemui.R; 55 import com.android.systemui.SysuiTestCase; 56 import com.android.systemui.plugins.qs.QSIconView; 57 import com.android.systemui.plugins.qs.QSTile; 58 import com.android.systemui.qs.QSHost; 59 import com.android.systemui.settings.UserTracker; 60 import com.android.systemui.util.concurrency.FakeExecutor; 61 import com.android.systemui.util.time.FakeSystemClock; 62 63 import org.junit.Before; 64 import org.junit.Test; 65 import org.junit.runner.RunWith; 66 import org.mockito.Answers; 67 import org.mockito.ArgumentCaptor; 68 import org.mockito.Captor; 69 import org.mockito.InOrder; 70 import org.mockito.Mock; 71 import org.mockito.MockitoAnnotations; 72 73 import java.util.ArrayList; 74 import java.util.Arrays; 75 import java.util.List; 76 import java.util.Set; 77 import java.util.concurrent.Executor; 78 79 @SmallTest 80 @RunWith(AndroidTestingRunner.class) 81 @TestableLooper.RunWithLooper 82 public class TileQueryHelperTest extends SysuiTestCase { 83 private static final String CURRENT_TILES = "internet,dnd,nfc"; 84 private static final String ONLY_STOCK_TILES = "internet,dnd"; 85 private static final String WITH_OTHER_TILES = "internet,dnd,other"; 86 // Note no nfc in stock tiles 87 private static final String STOCK_TILES = "internet,dnd,battery"; 88 private static final String ALL_TILES = "internet,dnd,nfc,battery"; 89 private static final Set<String> FACTORY_TILES = new ArraySet<>(); 90 private static final String TEST_PKG = "test_pkg"; 91 private static final String TEST_CLS = "test_cls"; 92 private static final String CUSTOM_TILE = "custom(" + TEST_PKG + "/" + TEST_CLS + ")"; 93 94 static { Arrays.asList(new String[]{"internet", "bt", "dnd", "inversion", "airplane", "work", "rotation", "flashlight", "location", "cast", "hotspot", "user", "battery", "saver", "night", "nfc"})95 FACTORY_TILES.addAll(Arrays.asList( 96 new String[]{"internet", "bt", "dnd", "inversion", "airplane", "work", 97 "rotation", "flashlight", "location", "cast", "hotspot", "user", "battery", 98 "saver", "night", "nfc"})); 99 FACTORY_TILES.add(CUSTOM_TILE); 100 } 101 102 @Mock 103 private TileQueryHelper.TileStateListener mListener; 104 @Mock 105 private QSHost mQSHost; 106 @Mock 107 private PackageManager mPackageManager; 108 @Mock 109 private UserTracker mUserTracker; 110 @Captor 111 private ArgumentCaptor<List<TileQueryHelper.TileInfo>> mCaptor; 112 113 private QSTile.State mState; 114 private TileQueryHelper mTileQueryHelper; 115 private FakeExecutor mMainExecutor; 116 private FakeExecutor mBgExecutor; 117 118 @Before setup()119 public void setup() { 120 MockitoAnnotations.initMocks(this); 121 mContext.setMockPackageManager(mPackageManager); 122 123 mState = new QSTile.State(); 124 doAnswer(invocation -> { 125 String spec = (String) invocation.getArguments()[0]; 126 if (FACTORY_TILES.contains(spec)) { 127 FakeQSTile tile = new FakeQSTile(mBgExecutor, mMainExecutor); 128 tile.setState(mState); 129 return tile; 130 } else { 131 return null; 132 } 133 } 134 ).when(mQSHost).createTile(anyString()); 135 FakeSystemClock clock = new FakeSystemClock(); 136 mMainExecutor = new FakeExecutor(clock); 137 mBgExecutor = new FakeExecutor(clock); 138 mTileQueryHelper = new TileQueryHelper( 139 mContext, mUserTracker, mMainExecutor, mBgExecutor); 140 mTileQueryHelper.setListener(mListener); 141 } 142 143 @Test testIsFinished_falseBeforeQuerying()144 public void testIsFinished_falseBeforeQuerying() { 145 assertFalse(mTileQueryHelper.isFinished()); 146 } 147 148 @Test testIsFinished_trueAfterQuerying()149 public void testIsFinished_trueAfterQuerying() { 150 mTileQueryHelper.queryTiles(mQSHost); 151 152 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 153 154 assertTrue(mTileQueryHelper.isFinished()); 155 } 156 157 @Test testQueryTiles_callsListenerTwice()158 public void testQueryTiles_callsListenerTwice() { 159 mTileQueryHelper.queryTiles(mQSHost); 160 161 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 162 163 verify(mListener, times(2)).onTilesChanged(any()); 164 } 165 166 @Test testQueryTiles_isFinishedFalseOnListenerCalls_thenTrueAfterCompletion()167 public void testQueryTiles_isFinishedFalseOnListenerCalls_thenTrueAfterCompletion() { 168 doAnswer(invocation -> { 169 assertFalse(mTileQueryHelper.isFinished()); 170 return null; 171 }).when(mListener).onTilesChanged(any()); 172 173 mTileQueryHelper.queryTiles(mQSHost); 174 175 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 176 177 assertTrue(mTileQueryHelper.isFinished()); 178 } 179 180 @Test testQueryTiles_correctTilesAndOrderOnlyStockTiles()181 public void testQueryTiles_correctTilesAndOrderOnlyStockTiles() { 182 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, 183 ONLY_STOCK_TILES); 184 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 185 STOCK_TILES); 186 187 mTileQueryHelper.queryTiles(mQSHost); 188 189 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 190 191 verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); 192 List<String> specs = new ArrayList<>(); 193 for (TileQueryHelper.TileInfo t : mCaptor.getValue()) { 194 specs.add(t.spec); 195 } 196 String tiles = TextUtils.join(",", specs); 197 assertThat(tiles, is(equalTo(STOCK_TILES))); 198 } 199 200 @Test testQueryTiles_correctTilesAndOrderOtherFactoryTiles()201 public void testQueryTiles_correctTilesAndOrderOtherFactoryTiles() { 202 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, 203 CURRENT_TILES); 204 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 205 STOCK_TILES); 206 207 mTileQueryHelper.queryTiles(mQSHost); 208 209 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 210 211 verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); 212 List<String> specs = new ArrayList<>(); 213 for (TileQueryHelper.TileInfo t : mCaptor.getValue()) { 214 specs.add(t.spec); 215 } 216 String tiles = TextUtils.join(",", specs); 217 assertThat(tiles, is(equalTo(ALL_TILES))); 218 } 219 220 @Test testQueryTiles_otherTileNotIncluded()221 public void testQueryTiles_otherTileNotIncluded() { 222 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, 223 WITH_OTHER_TILES); 224 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 225 STOCK_TILES); 226 227 mTileQueryHelper.queryTiles(mQSHost); 228 229 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 230 231 verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); 232 List<String> specs = new ArrayList<>(); 233 for (TileQueryHelper.TileInfo t : mCaptor.getValue()) { 234 specs.add(t.spec); 235 } 236 assertFalse(specs.contains("other")); 237 } 238 239 @Test testCustomTileNotCreated()240 public void testCustomTileNotCreated() { 241 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, 242 CUSTOM_TILE); 243 mTileQueryHelper.queryTiles(mQSHost); 244 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 245 verify(mQSHost, never()).createTile(CUSTOM_TILE); 246 } 247 248 @Test testThirdPartyTilesInactive()249 public void testThirdPartyTilesInactive() { 250 ResolveInfo resolveInfo = new ResolveInfo(); 251 ServiceInfo serviceInfo = mock(ServiceInfo.class, Answers.RETURNS_MOCKS); 252 resolveInfo.serviceInfo = serviceInfo; 253 serviceInfo.packageName = TEST_PKG; 254 serviceInfo.name = TEST_CLS; 255 serviceInfo.icon = R.drawable.android; 256 serviceInfo.permission = Manifest.permission.BIND_QUICK_SETTINGS_TILE; 257 serviceInfo.applicationInfo = mock(ApplicationInfo.class, Answers.RETURNS_MOCKS); 258 serviceInfo.applicationInfo.icon = R.drawable.android; 259 List<ResolveInfo> list = new ArrayList<>(); 260 list.add(resolveInfo); 261 when(mPackageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())).thenReturn(list); 262 263 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, ""); 264 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 265 ""); 266 267 mTileQueryHelper.queryTiles(mQSHost); 268 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 269 270 verify(mListener, atLeastOnce()).onTilesChanged(mCaptor.capture()); 271 List<TileQueryHelper.TileInfo> tileInfos = mCaptor.getValue(); 272 assertEquals(1, tileInfos.size()); 273 assertEquals(Tile.STATE_INACTIVE, tileInfos.get(0).state.state); 274 } 275 276 @Test testQueryTiles_nullSetting()277 public void testQueryTiles_nullSetting() { 278 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null); 279 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 280 STOCK_TILES); 281 mTileQueryHelper.queryTiles(mQSHost); 282 } 283 284 @Test testQueryTiles_notAvailableDestroyed_tileSpecIsSet()285 public void testQueryTiles_notAvailableDestroyed_tileSpecIsSet() { 286 Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.QS_TILES, null); 287 288 QSTile t = mock(QSTile.class); 289 when(mQSHost.createTile("hotspot")).thenReturn(t); 290 291 mContext.getOrCreateTestableResources().addOverride(R.string.quick_settings_tiles_stock, 292 "hotspot"); 293 294 mTileQueryHelper.queryTiles(mQSHost); 295 296 FakeExecutor.exhaustExecutors(mMainExecutor, mBgExecutor); 297 InOrder verifier = inOrder(t); 298 verifier.verify(t).setTileSpec("hotspot"); 299 verifier.verify(t).destroy(); 300 } 301 302 private static class FakeQSTile implements QSTile { 303 304 private String mSpec = ""; 305 private List<Callback> mCallbacks = new ArrayList<>(); 306 private boolean mRefreshed; 307 private boolean mListening; 308 private State mState = new State(); 309 private final Executor mBgExecutor; 310 private final Executor mMainExecutor; 311 FakeQSTile(Executor bgExecutor, Executor mainExecutor)312 FakeQSTile(Executor bgExecutor, Executor mainExecutor) { 313 mBgExecutor = bgExecutor; 314 mMainExecutor = mainExecutor; 315 } 316 317 @Override getTileSpec()318 public String getTileSpec() { 319 return mSpec; 320 } 321 322 @Override isAvailable()323 public boolean isAvailable() { 324 return true; 325 } 326 327 @Override setTileSpec(String tileSpec)328 public void setTileSpec(String tileSpec) { 329 mSpec = tileSpec; 330 } 331 setState(State state)332 public void setState(State state) { 333 mState = state; 334 notifyChangedState(mState); 335 } 336 337 @Override refreshState()338 public void refreshState() { 339 mBgExecutor.execute(() -> { 340 mRefreshed = true; 341 notifyChangedState(mState); 342 }); 343 } 344 notifyChangedState(State state)345 private void notifyChangedState(State state) { 346 List<Callback> callbacks = new ArrayList<>(mCallbacks); 347 callbacks.forEach(callback -> callback.onStateChanged(state)); 348 } 349 350 @Override addCallback(Callback callback)351 public void addCallback(Callback callback) { 352 mCallbacks.add(callback); 353 } 354 355 @Override removeCallback(Callback callback)356 public void removeCallback(Callback callback) { 357 mCallbacks.remove(callback); 358 } 359 360 @Override removeCallbacks()361 public void removeCallbacks() { 362 mCallbacks.clear(); 363 } 364 365 @Override setListening(Object client, boolean listening)366 public void setListening(Object client, boolean listening) { 367 if (listening) { 368 mMainExecutor.execute(() -> { 369 mListening = true; 370 refreshState(); 371 }); 372 } 373 } 374 375 @Override isListening()376 public boolean isListening() { 377 return mListening; 378 } 379 380 @Override getTileLabel()381 public CharSequence getTileLabel() { 382 return mSpec; 383 } 384 385 @Override getState()386 public State getState() { 387 return mState; 388 } 389 390 @Override isTileReady()391 public boolean isTileReady() { 392 return mListening && mRefreshed; 393 } 394 395 @Override createTileView(Context context)396 public QSIconView createTileView(Context context) { 397 return null; 398 } 399 400 @Override click(@ullable View view)401 public void click(@Nullable View view) {} 402 403 @Override secondaryClick(@ullable View view)404 public void secondaryClick(@Nullable View view) {} 405 406 @Override longClick(@ullable View view)407 public void longClick(@Nullable View view) {} 408 409 @Override userSwitch(int currentUser)410 public void userSwitch(int currentUser) {} 411 412 @Override getMetricsCategory()413 public int getMetricsCategory() { 414 return 0; 415 } 416 417 @Override getInstanceId()418 public InstanceId getInstanceId() { 419 return null; 420 } 421 422 @Override setDetailListening(boolean show)423 public void setDetailListening(boolean show) {} 424 425 @Override destroy()426 public void destroy() {} 427 } 428 } 429