1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.applications.appinfo;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.ArgumentMatchers.anyBoolean;
23 import static org.mockito.ArgumentMatchers.anyInt;
24 import static org.mockito.ArgumentMatchers.anyString;
25 import static org.mockito.ArgumentMatchers.eq;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.doNothing;
28 import static org.mockito.Mockito.doReturn;
29 import static org.mockito.Mockito.doThrow;
30 import static org.mockito.Mockito.mock;
31 import static org.mockito.Mockito.never;
32 import static org.mockito.Mockito.spy;
33 import static org.mockito.Mockito.verify;
34 import static org.mockito.Mockito.when;
35 
36 import android.app.ActivityManager;
37 import android.app.Application;
38 import android.app.admin.DevicePolicyManager;
39 import android.app.settings.SettingsEnums;
40 import android.content.Context;
41 import android.content.Intent;
42 import android.content.om.OverlayInfo;
43 import android.content.om.OverlayManager;
44 import android.content.pm.ApplicationInfo;
45 import android.content.pm.PackageInfo;
46 import android.content.pm.PackageManager;
47 import android.os.RemoteException;
48 import android.os.UserManager;
49 import android.util.ArraySet;
50 import android.view.View;
51 
52 import androidx.preference.PreferenceScreen;
53 
54 import com.android.settings.R;
55 import com.android.settings.SettingsActivity;
56 import com.android.settings.core.InstrumentedPreferenceFragment;
57 import com.android.settings.testutils.FakeFeatureFactory;
58 import com.android.settingslib.applications.AppUtils;
59 import com.android.settingslib.applications.ApplicationsState;
60 import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
61 import com.android.settingslib.core.lifecycle.Lifecycle;
62 import com.android.settingslib.widget.ActionButtonsPreference;
63 
64 import org.junit.After;
65 import org.junit.Before;
66 import org.junit.Test;
67 import org.junit.runner.RunWith;
68 import org.mockito.Answers;
69 import org.mockito.ArgumentCaptor;
70 import org.mockito.Mock;
71 import org.mockito.MockitoAnnotations;
72 import org.mockito.stubbing.Answer;
73 import org.robolectric.RobolectricTestRunner;
74 import org.robolectric.RuntimeEnvironment;
75 import org.robolectric.annotation.Config;
76 import org.robolectric.annotation.Implementation;
77 import org.robolectric.annotation.Implements;
78 import org.robolectric.annotation.Resetter;
79 import org.robolectric.util.ReflectionHelpers;
80 
81 import java.util.Set;
82 
83 @RunWith(RobolectricTestRunner.class)
84 public class AppButtonsPreferenceControllerTest {
85 
86     private static final String PACKAGE_NAME = "com.android.settings";
87     private static final String RRO_PACKAGE_NAME = "com.android.settings.overlay";
88     private static final String RESOURCE_STRING = "string";
89     private static final boolean ALL_USERS = false;
90     private static final boolean DISABLE_AFTER_INSTALL = true;
91     private static final int REQUEST_UNINSTALL = 0;
92     private static final int REQUEST_REMOVE_DEVICE_ADMIN = 1;
93     private static final OverlayInfo OVERLAY_DISABLED = createFakeOverlay("overlay", false, 1);
94     private static final OverlayInfo OVERLAY_ENABLED = createFakeOverlay("overlay", true, 1);
95 
96     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
97     private SettingsActivity mSettingsActivity;
98     @Mock
99     private TestFragment mFragment;
100     @Mock
101     private Lifecycle mLifecycle;
102     @Mock
103     private ApplicationsState mState;
104     @Mock
105     private ApplicationsState.AppEntry mAppEntry;
106     @Mock
107     private ApplicationInfo mAppInfo;
108     @Mock
109     private OverlayManager mOverlayManager;
110     @Mock
111     private PackageManager mPackageManger;
112     @Mock
113     private DevicePolicyManager mDpm;
114     @Mock
115     private ActivityManager mAm;
116     @Mock
117     private UserManager mUserManager;
118     @Mock
119     private PackageInfo mPackageInfo;
120 
121     private Context mContext;
122     private Intent mUninstallIntent;
123     private ActionButtonsPreference mButtonPrefs;
124     private AppButtonsPreferenceController mController;
125 
126     @Before
setUp()127     public void setUp() {
128         MockitoAnnotations.initMocks(this);
129 
130         FakeFeatureFactory.setupForTest();
131         mContext = RuntimeEnvironment.application;
132         doReturn(mDpm).when(mSettingsActivity).getSystemService(Context.DEVICE_POLICY_SERVICE);
133         doReturn(mUserManager).when(mSettingsActivity).getSystemService(Context.USER_SERVICE);
134         doReturn(mPackageManger).when(mSettingsActivity).getPackageManager();
135         doReturn(mAm).when(mSettingsActivity).getSystemService(Context.ACTIVITY_SERVICE);
136         doReturn(mOverlayManager).when(mSettingsActivity).
137                 getSystemService(OverlayManager.class);
138         doReturn(mAppEntry).when(mState).getEntry(anyString(), anyInt());
139         doReturn(mContext).when(mSettingsActivity).getApplicationContext();
140         when(mSettingsActivity.getResources().getString(anyInt())).thenReturn(RESOURCE_STRING);
141 
142         mController = spy(new AppButtonsPreferenceController(mSettingsActivity, mFragment,
143                 mLifecycle, PACKAGE_NAME, mState, REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN));
144 
145         mAppEntry.info = mAppInfo;
146         mAppInfo.packageName = PACKAGE_NAME;
147         mAppInfo.flags = 0;
148         mPackageInfo.packageName = PACKAGE_NAME;
149         mPackageInfo.applicationInfo = mAppInfo;
150 
151         mButtonPrefs = createMock();
152         mController.mButtonsPref = mButtonPrefs;
153         mController.mPackageInfo = mPackageInfo;
154 
155         final ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
156         Answer<Void> callable = invocation -> {
157             mUninstallIntent = captor.getValue();
158             return null;
159         };
160         doAnswer(callable).when(mFragment).startActivityForResult(captor.capture(), anyInt());
161     }
162 
163     @After
tearDown()164     public void tearDown() {
165         ShadowAppUtils.reset();
166     }
167 
168     @Test
169     @Config(shadows = ShadowAppUtils.class)
isAvailable_validPackageName_isTrue()170     public void isAvailable_validPackageName_isTrue() {
171         assertThat(mController.isAvailable()).isTrue();
172     }
173 
174     @Test
isAvailable_nullPackageName_isFalse()175     public void isAvailable_nullPackageName_isFalse() {
176         final AppButtonsPreferenceController controller = spy(
177                 new AppButtonsPreferenceController(mSettingsActivity, mFragment,
178                         mLifecycle, null, mState, REQUEST_UNINSTALL, REQUEST_REMOVE_DEVICE_ADMIN));
179 
180         assertThat(controller.isAvailable()).isFalse();
181     }
182 
183     @Test
retrieveAppEntry_hasAppEntry_notNull()184     public void retrieveAppEntry_hasAppEntry_notNull()
185             throws PackageManager.NameNotFoundException {
186         doReturn(mPackageInfo).when(mPackageManger).getPackageInfo(anyString(), anyInt());
187 
188         mController.retrieveAppEntry();
189 
190         assertThat(mController.mAppEntry).isNotNull();
191         assertThat(mController.mPackageInfo).isNotNull();
192     }
193 
194     @Test
retrieveAppEntry_noAppEntry_null()195     public void retrieveAppEntry_noAppEntry_null() throws PackageManager.NameNotFoundException {
196         doReturn(null).when(mState).getEntry(eq(PACKAGE_NAME), anyInt());
197         doReturn(mPackageInfo).when(mPackageManger).getPackageInfo(anyString(), anyInt());
198 
199         mController.retrieveAppEntry();
200 
201         assertThat(mController.mAppEntry).isNull();
202         assertThat(mController.mPackageInfo).isNull();
203     }
204 
205     @Test
retrieveAppEntry_throwException_null()206     public void retrieveAppEntry_throwException_null() throws
207             PackageManager.NameNotFoundException {
208         doReturn(mAppEntry).when(mState).getEntry(anyString(), anyInt());
209         doThrow(new PackageManager.NameNotFoundException()).when(mPackageManger).getPackageInfo(
210                 anyString(), anyInt());
211 
212         mController.retrieveAppEntry();
213 
214         assertThat(mController.mAppEntry).isNotNull();
215         assertThat(mController.mPackageInfo).isNull();
216     }
217 
218     @Test
updateOpenButton_noLaunchIntent_buttonShouldBeDisable()219     public void updateOpenButton_noLaunchIntent_buttonShouldBeDisable() {
220         mController.updateOpenButton();
221 
222         verify(mButtonPrefs).setButton1Visible(false);
223     }
224 
225     @Test
updateOpenButton_haveLaunchIntent_buttonShouldBeEnable()226     public void updateOpenButton_haveLaunchIntent_buttonShouldBeEnable() {
227         doReturn(new Intent()).when(mPackageManger).getLaunchIntentForPackage(anyString());
228 
229         mController.updateOpenButton();
230 
231         verify(mButtonPrefs).setButton1Visible(true);
232     }
233 
234     @Test
updateUninstallButton_isSystemApp_handleAsDisableableButton()235     public void updateUninstallButton_isSystemApp_handleAsDisableableButton() {
236         doReturn(false).when(mController).handleDisableable();
237         mAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
238 
239         mController.updateUninstallButton();
240 
241         verify(mController).handleDisableable();
242         verify(mButtonPrefs).setButton2Enabled(false);
243     }
244 
245     @Test
246     @Config(shadows = ShadowAppUtils.class)
isAvailable_nonInstantApp()247     public void isAvailable_nonInstantApp() {
248         mController.mAppEntry = mAppEntry;
249         assertThat(mController.isAvailable()).isTrue();
250     }
251 
252     @Test
isAvailable_instantApp()253     public void isAvailable_instantApp() {
254         mController.mAppEntry = mAppEntry;
255         ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider",
256                 new InstantAppDataProvider() {
257                     @Override
258                     public boolean isInstantApp(ApplicationInfo info) {
259                         return true;
260                     }
261                 });
262         assertThat(mController.isAvailable()).isFalse();
263     }
264 
265     @Test
updateUninstallButton_isDeviceAdminApp_setButtonDisable()266     public void updateUninstallButton_isDeviceAdminApp_setButtonDisable() {
267         doReturn(true).when(mController).handleDisableable();
268         mAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
269         doReturn(true).when(mDpm).packageHasActiveAdmins(anyString());
270 
271         mController.updateUninstallButton();
272 
273         verify(mController).handleDisableable();
274         verify(mButtonPrefs).setButton2Enabled(false);
275     }
276 
277     @Test
updateUninstallButton_isProfileOrDeviceOwner_setButtonDisable()278     public void updateUninstallButton_isProfileOrDeviceOwner_setButtonDisable() {
279         doReturn(true).when(mDpm).isDeviceOwnerAppOnAnyUser(anyString());
280 
281         mController.updateUninstallButton();
282 
283         verify(mButtonPrefs).setButton2Enabled(false);
284     }
285 
286     @Test
updateUninstallButton_isDeviceProvisioningApp_setButtonDisable()287     public void updateUninstallButton_isDeviceProvisioningApp_setButtonDisable() {
288         doReturn(true).when(mDpm).isDeviceOwnerAppOnAnyUser(anyString());
289         when(mSettingsActivity.getResources().getString(anyInt())).thenReturn(PACKAGE_NAME);
290 
291         mController.updateUninstallButton();
292 
293         verify(mButtonPrefs).setButton2Enabled(false);
294     }
295 
296     @Test
updateUninstallButton_isUninstallInQueue_setButtonDisable()297     public void updateUninstallButton_isUninstallInQueue_setButtonDisable() {
298         doReturn(true).when(mDpm).isUninstallInQueue(any());
299 
300         mController.updateUninstallButton();
301 
302         verify(mButtonPrefs).setButton2Enabled(false);
303     }
304 
305     @Test
updateUninstallButton_isHomeAppAndBundled_setButtonDisable()306     public void updateUninstallButton_isHomeAppAndBundled_setButtonDisable() {
307         mAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
308         mController.mHomePackages.add(PACKAGE_NAME);
309 
310         mController.updateUninstallButton();
311 
312         verify(mButtonPrefs).setButton2Enabled(false);
313     }
314 
315     @Test
updateUninstallButton_isSystemRro_setButtonDisable()316     public void updateUninstallButton_isSystemRro_setButtonDisable() {
317         mAppInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
318 
319         when(mAppInfo.isResourceOverlay()).thenReturn(true);
320 
321         mController.updateUninstallButton();
322 
323         verify(mButtonPrefs).setButton2Enabled(false);
324     }
325 
326     @Test
updateUninstallButton_isNonSystemRro_setButtonDisable()327     public void updateUninstallButton_isNonSystemRro_setButtonDisable()
328             throws RemoteException {
329         when(mAppInfo.isResourceOverlay()).thenReturn(true);
330         when(mOverlayManager.getOverlayInfo(anyString(), any()))
331                 .thenReturn(OVERLAY_ENABLED);
332 
333         mController.updateUninstallButton();
334 
335         verify(mButtonPrefs).setButton2Enabled(false);
336     }
337 
338     @Test
updateUninstallButton_isNonSystemRro_setButtonEnable()339     public void updateUninstallButton_isNonSystemRro_setButtonEnable()
340             throws RemoteException {
341         when(mAppInfo.isResourceOverlay()).thenReturn(true);
342         when(mOverlayManager.getOverlayInfo(anyString(), any()))
343                 .thenReturn(OVERLAY_DISABLED);
344 
345         mController.updateUninstallButton();
346 
347         verify(mButtonPrefs).setButton2Enabled(true);
348     }
349 
350     @Test
updateForceStopButton_HasActiveAdmins_setButtonDisable()351     public void updateForceStopButton_HasActiveAdmins_setButtonDisable() {
352         doReturn(true).when(mDpm).packageHasActiveAdmins(anyString());
353 
354         mController.updateForceStopButton();
355 
356         verify(mController).updateForceStopButtonInner(false);
357     }
358 
359     @Test
updateForceStopButton_AppNotStopped_setButtonEnable()360     public void updateForceStopButton_AppNotStopped_setButtonEnable() {
361         mController.updateForceStopButton();
362 
363         verify(mController).updateForceStopButtonInner(true);
364     }
365 
366     @Test
uninstallPkg_intentSent()367     public void uninstallPkg_intentSent() {
368         mController.uninstallPkg(PACKAGE_NAME, ALL_USERS, DISABLE_AFTER_INSTALL);
369 
370         verify(mFragment).startActivityForResult(any(), eq(REQUEST_UNINSTALL));
371         assertThat(
372                 mUninstallIntent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true))
373                 .isEqualTo(ALL_USERS);
374         assertThat(mUninstallIntent.getAction()).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE);
375         assertThat(mController.mDisableAfterUninstall).isEqualTo(DISABLE_AFTER_INSTALL);
376     }
377 
378     @Test
forceStopPackage_methodInvokedAndUpdated()379     public void forceStopPackage_methodInvokedAndUpdated() {
380         final ApplicationsState.AppEntry appEntry = mock(ApplicationsState.AppEntry.class);
381         doReturn(appEntry).when(mState).getEntry(anyString(), anyInt());
382         doNothing().when(mController).updateForceStopButton();
383 
384         mController.forceStopPackage(PACKAGE_NAME);
385 
386         verify(mAm).forceStopPackage(PACKAGE_NAME);
387         assertThat(mController.mAppEntry).isSameAs(appEntry);
388         verify(mController).updateForceStopButton();
389     }
390 
391     @Test
handleDisableable_isHomeApp_notControllable()392     public void handleDisableable_isHomeApp_notControllable() {
393         mController.mHomePackages.add(PACKAGE_NAME);
394 
395         final boolean controllable = mController.handleDisableable();
396 
397         verify(mButtonPrefs).setButton2Text(R.string.disable_text);
398         assertThat(controllable).isFalse();
399     }
400 
401     @Test
handleDisableable_isAppEnabled_controllable()402     public void handleDisableable_isAppEnabled_controllable() {
403         mAppEntry.info.enabled = true;
404         mAppEntry.info.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
405         doReturn(false).when(mController).isSystemPackage(any(), any(), any());
406 
407         final boolean controllable = mController.handleDisableable();
408 
409         verify(mButtonPrefs).setButton2Text(R.string.disable_text);
410         assertThat(controllable).isTrue();
411     }
412 
413     @Test
handleDisableable_isAppDisabled_controllable()414     public void handleDisableable_isAppDisabled_controllable() {
415         mAppEntry.info.enabled = false;
416         mAppEntry.info.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
417         doReturn(false).when(mController).isSystemPackage(any(), any(), any());
418 
419         final boolean controllable = mController.handleDisableable();
420 
421         verify(mButtonPrefs).setButton2Text(R.string.enable_text);
422         assertThat(controllable).isTrue();
423     }
424 
425     @Test
handleActivityResult_packageUninstalled_shouldFinishPrefernecePanel()426     public void handleActivityResult_packageUninstalled_shouldFinishPrefernecePanel() {
427         doReturn(false).when(mController).refreshUi();
428 
429         mController.handleActivityResult(REQUEST_UNINSTALL, 0, mock(Intent.class));
430 
431         verify(mSettingsActivity).finishPreferencePanel(anyInt(), any(Intent.class));
432     }
433 
434     @Test
refreshUi_packageNull_shouldNotCrash()435     public void refreshUi_packageNull_shouldNotCrash() {
436         mController.mPackageName = null;
437 
438         // Should not crash in this method
439         assertThat(mController.refreshUi()).isFalse();
440     }
441 
442     @Test
onPackageListChanged_available_shouldRefreshUi()443     public void onPackageListChanged_available_shouldRefreshUi() {
444         doReturn(AppButtonsPreferenceController.AVAILABLE)
445                 .when(mController).getAvailabilityStatus();
446         doReturn(true).when(mController).refreshUi();
447 
448         mController.onPackageListChanged();
449 
450         verify(mController).refreshUi();
451     }
452 
453     @Test
onPackageListChanged_notAvailable_shouldNotRefreshUiAndNoCrash()454     public void onPackageListChanged_notAvailable_shouldNotRefreshUiAndNoCrash() {
455         doReturn(AppButtonsPreferenceController.DISABLED_FOR_USER)
456                 .when(mController).getAvailabilityStatus();
457 
458         mController.onPackageListChanged();
459 
460         verify(mController, never()).refreshUi();
461         // Should not crash in this method
462     }
463 
464     @Test
465     @Config(shadows = ShadowAppUtils.class)
getAvailabilityStatus_systemModule()466     public void getAvailabilityStatus_systemModule() {
467         ShadowAppUtils.addHiddenModule(mController.mPackageName);
468         assertThat(mController.getAvailabilityStatus()).isEqualTo(
469                 AppButtonsPreferenceController.DISABLED_FOR_USER);
470     }
471 
472     /**
473      * The test fragment which implements
474      * {@link ButtonActionDialogFragment.AppButtonsDialogListener}
475      */
476     public static class TestFragment extends InstrumentedPreferenceFragment
477             implements ButtonActionDialogFragment.AppButtonsDialogListener {
478 
479         @Override
handleDialogClick(int type)480         public void handleDialogClick(int type) {
481             // Do nothing
482         }
483 
484         @Override
getMetricsCategory()485         public int getMetricsCategory() {
486             return SettingsEnums.PAGE_UNKNOWN;
487         }
488     }
489 
createMock()490     private ActionButtonsPreference createMock() {
491         final ActionButtonsPreference pref = mock(ActionButtonsPreference.class);
492         when(pref.setButton2Text(anyInt())).thenReturn(pref);
493         when(pref.setButton2Icon(anyInt())).thenReturn(pref);
494         when(pref.setButton2Enabled(anyBoolean())).thenReturn(pref);
495         when(pref.setButton2Visible(anyBoolean())).thenReturn(pref);
496         when(pref.setButton2OnClickListener(any(View.OnClickListener.class))).thenReturn(pref);
497 
498         return pref;
499     }
500 
createFakeOverlay(String pkg, boolean enabled, int priority)501     private static OverlayInfo createFakeOverlay(String pkg, boolean enabled, int priority) {
502         final int state = (enabled) ? OverlayInfo.STATE_ENABLED : OverlayInfo.STATE_DISABLED;
503         return new OverlayInfo(pkg /* packageName */,
504                 "target.package" /* targetPackageName */,
505                 "theme" /* targetOverlayableName */,
506                 "category", /* category */
507                 "package", /* baseCodePath */
508                 state,
509                 0 /* userId */,
510                 priority,
511                 false /* isStatic */);
512     }
513 
514     @Implements(AppUtils.class)
515     public static class ShadowAppUtils {
516 
517         public static Set<String> sSystemModules = new ArraySet<>();
518 
519         @Resetter
reset()520         public static void reset() {
521             sSystemModules.clear();
522         }
523 
addHiddenModule(String pkg)524         public static void addHiddenModule(String pkg) {
525             sSystemModules.add(pkg);
526         }
527 
528         @Implementation
isInstant(ApplicationInfo info)529         protected static boolean isInstant(ApplicationInfo info) {
530             return false;
531         }
532 
533         @Implementation
isSystemModule(Context context, String packageName)534         protected static boolean isSystemModule(Context context, String packageName) {
535             return sSystemModules.contains(packageName);
536         }
537     }
538 }
539