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.webview;
18 
19 import static android.provider.Settings.ACTION_WEBVIEW_SETTINGS;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.eq;
25 import static org.mockito.Mockito.doNothing;
26 import static org.mockito.Mockito.doReturn;
27 import static org.mockito.Mockito.mock;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.spy;
30 import static org.mockito.Mockito.times;
31 import static org.mockito.Mockito.verify;
32 import static org.mockito.Mockito.when;
33 
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.pm.ApplicationInfo;
37 import android.content.pm.PackageInfo;
38 import android.content.pm.UserInfo;
39 import android.graphics.drawable.ColorDrawable;
40 import android.webkit.UserPackage;
41 
42 import androidx.fragment.app.FragmentActivity;
43 
44 import com.android.settingslib.applications.DefaultAppInfo;
45 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
46 import com.android.settingslib.widget.RadioButtonPreference;
47 
48 import org.junit.After;
49 import org.junit.Before;
50 import org.junit.Test;
51 import org.junit.runner.RunWith;
52 import org.mockito.Mock;
53 import org.mockito.MockitoAnnotations;
54 import org.robolectric.RobolectricTestRunner;
55 import org.robolectric.RuntimeEnvironment;
56 import org.robolectric.Shadows;
57 import org.robolectric.shadow.api.Shadow;
58 import org.robolectric.shadows.ShadowPackageManager;
59 import org.robolectric.shadows.ShadowUserManager;
60 import org.robolectric.util.ReflectionHelpers;
61 
62 import java.util.Arrays;
63 import java.util.Collections;
64 
65 @RunWith(RobolectricTestRunner.class)
66 public class WebViewAppPickerTest {
67 
68     private final static String PACKAGE_NAME = "com.example.test";
69     private final static String PACKAGE_VERSION = "1.0.0";
70 
71     @Mock
72     private FragmentActivity mActivity;
73 
74     private Context mContext;
75     private UserInfo mFirstUser;
76     private UserInfo mSecondUser;
77     private ShadowPackageManager mPackageManager;
78     private WebViewAppPicker mPicker;
79     private WebViewUpdateServiceWrapper mWvusWrapper;
80     private ShadowUserManager mUserManager;
81 
82     @Before
setUp()83     public void setUp() {
84         MockitoAnnotations.initMocks(this);
85         mContext = spy(RuntimeEnvironment.application);
86         mUserManager = Shadow.extract(mContext.getSystemService(Context.USER_SERVICE));
87         mPackageManager = Shadows.shadowOf(mContext.getPackageManager());
88 
89         final ApplicationInfo applicationInfo = new ApplicationInfo();
90         applicationInfo.name = PACKAGE_NAME;
91         applicationInfo.uid = 0;
92         applicationInfo.flags = 0;
93         applicationInfo.packageName = PACKAGE_NAME;
94 
95         final PackageInfo packageInfo = new PackageInfo();
96         packageInfo.packageName = PACKAGE_NAME;
97         packageInfo.applicationInfo = applicationInfo;
98         packageInfo.versionName = PACKAGE_VERSION;
99         mPackageManager.addPackage(packageInfo);
100         mPackageManager.setUnbadgedApplicationIcon(PACKAGE_NAME, new ColorDrawable());
101 
102         mFirstUser = new UserInfo(0, "FIRST_USER", 0);
103         mSecondUser = new UserInfo(0, "SECOND_USER", 0);
104         mPicker = new WebViewAppPicker();
105         mPicker = spy(mPicker);
106         doNothing().when(mPicker).updateCandidates();
107         doNothing().when(mPicker).updateCheckedState(any());
108         doReturn(mActivity).when(mPicker).getActivity();
109 
110         ReflectionHelpers.setField(mPicker, "mMetricsFeatureProvider",
111                 mock(MetricsFeatureProvider.class));
112         mWvusWrapper = mock(WebViewUpdateServiceWrapper.class);
113         mPicker.setWebViewUpdateServiceWrapper(mWvusWrapper);
114     }
115 
116     @After
tearDown()117     public void tearDown() {
118         mPackageManager.removePackage(PACKAGE_NAME);
119     }
120 
121     @Test
testClickingItemChangesProvider()122     public void testClickingItemChangesProvider() {
123         testSuccessfulClickChangesProvider();
124     }
125 
126     @Test
testFailingClick()127     public void testFailingClick() {
128         testFailingClickUpdatesSetting();
129     }
130 
131     @Test
testClickingItemInActivityModeChangesProviderAndFinishes()132     public void testClickingItemInActivityModeChangesProviderAndFinishes() {
133         useWebViewSettingIntent();
134         testSuccessfulClickChangesProvider();
135         verify(mActivity, times(1)).finish();
136     }
137 
138     @Test
testFailingClickInActivityMode()139     public void testFailingClickInActivityMode() {
140         useWebViewSettingIntent();
141         testFailingClick();
142     }
143 
144     @Test
testDisabledPackageShownAsDisabled()145     public void testDisabledPackageShownAsDisabled() {
146         DefaultAppInfo webviewAppInfo = mPicker.createDefaultAppInfo(mContext,
147                 mContext.getPackageManager(),
148                 createApplicationInfo(PACKAGE_NAME), "disabled");
149 
150         RadioButtonPreference preference = mock(RadioButtonPreference.class);
151         mPicker.bindPreference(preference, PACKAGE_NAME, webviewAppInfo, null);
152         mPicker.bindPreferenceExtra(preference, PACKAGE_NAME, webviewAppInfo, null, null);
153         verify(preference, times(1)).setEnabled(eq(false));
154         verify(preference, never()).setEnabled(eq(true));
155     }
156 
157     @Test
testEnabledPackageShownAsEnabled()158     public void testEnabledPackageShownAsEnabled() {
159         String disabledReason = "";
160         DefaultAppInfo webviewAppInfo = mPicker.createDefaultAppInfo(mContext,
161                 mContext.getPackageManager(),
162                 createApplicationInfo(PACKAGE_NAME), disabledReason);
163 
164         RadioButtonPreference preference = mock(RadioButtonPreference.class);
165         mPicker.bindPreference(preference, PACKAGE_NAME, webviewAppInfo, null);
166         mPicker.bindPreferenceExtra(preference, PACKAGE_NAME, webviewAppInfo, null, null);
167         verify(preference, times(1)).setEnabled(eq(true));
168         verify(preference, never()).setEnabled(eq(false));
169     }
170 
171     @Test
testDisabledPackageShowsDisabledReasonSummary()172     public void testDisabledPackageShowsDisabledReasonSummary() {
173         String disabledReason = "disabled";
174         DefaultAppInfo webviewAppInfo = mPicker.createDefaultAppInfo(mContext,
175                 mContext.getPackageManager(),
176                 createApplicationInfo(PACKAGE_NAME), disabledReason);
177 
178         RadioButtonPreference preference = mock(RadioButtonPreference.class);
179         mPicker.bindPreference(preference, PACKAGE_NAME, webviewAppInfo, null);
180         mPicker.bindPreferenceExtra(preference, PACKAGE_NAME, webviewAppInfo, null, null);
181         verify(preference, times(1)).setSummary(eq(disabledReason));
182         // Ensure we haven't called setSummary several times.
183         verify(preference, times(1)).setSummary(any());
184     }
185 
186     @Test
testEnabledPackageShowsEmptySummary()187     public void testEnabledPackageShowsEmptySummary() {
188         DefaultAppInfo webviewAppInfo = mPicker.createDefaultAppInfo(mContext,
189                 mContext.getPackageManager(),
190                 createApplicationInfo(PACKAGE_NAME), null);
191 
192         RadioButtonPreference preference = mock(RadioButtonPreference.class);
193         mPicker.bindPreference(preference, PACKAGE_NAME, webviewAppInfo, null);
194         mPicker.bindPreferenceExtra(preference, PACKAGE_NAME, webviewAppInfo, null, null);
195         verify(preference, never()).setSummary(any());
196     }
197 
198     @Test
testFinishIfNotAdmin()199     public void testFinishIfNotAdmin() {
200         mUserManager.setIsAdminUser(false);
201         mPicker.onAttach(mContext);
202         verify(mActivity, times(1)).finish();
203     }
204 
205     @Test
testNotFinishedIfAdmin()206     public void testNotFinishedIfAdmin() {
207         mUserManager.setIsAdminUser(true);
208         mPicker.onAttach(mContext);
209         verify(mActivity, never()).finish();
210     }
211 
212     @Test
testDisabledReasonNullIfPackagesOk()213     public void testDisabledReasonNullIfPackagesOk() {
214         UserPackage packageForFirstUser = mock(UserPackage.class);
215         when(packageForFirstUser.isEnabledPackage()).thenReturn(true);
216         when(packageForFirstUser.isInstalledPackage()).thenReturn(true);
217 
218         UserPackage packageForSecondUser = mock(UserPackage.class);
219         when(packageForSecondUser.isEnabledPackage()).thenReturn(true);
220         when(packageForSecondUser.isInstalledPackage()).thenReturn(true);
221 
222         WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class);
223         when(wvusWrapper.getPackageInfosAllUsers(any(), eq(PACKAGE_NAME)))
224                 .thenReturn(Arrays.asList(packageForFirstUser, packageForSecondUser));
225 
226         assertThat(mPicker.getDisabledReason(wvusWrapper, mContext, PACKAGE_NAME)).isNull();
227     }
228 
229     @Test
testDisabledReasonForSingleUserDisabledPackage()230     public void testDisabledReasonForSingleUserDisabledPackage() {
231         UserPackage packageForFirstUser = mock(UserPackage.class);
232         when(packageForFirstUser.isEnabledPackage()).thenReturn(false);
233         when(packageForFirstUser.isInstalledPackage()).thenReturn(true);
234         when(packageForFirstUser.getUserInfo()).thenReturn(mFirstUser);
235 
236         WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class);
237         when(wvusWrapper.getPackageInfosAllUsers(any(), eq(PACKAGE_NAME)))
238                 .thenReturn(Collections.singletonList(packageForFirstUser));
239 
240         final String expectedReason = String.format("(disabled for user %s)", mFirstUser.name);
241         assertThat(mPicker.getDisabledReason(wvusWrapper, mContext, PACKAGE_NAME))
242                 .isEqualTo(expectedReason);
243     }
244 
245     @Test
testDisabledReasonForSingleUserUninstalledPackage()246     public void testDisabledReasonForSingleUserUninstalledPackage() {
247         UserPackage packageForFirstUser = mock(UserPackage.class);
248         when(packageForFirstUser.isEnabledPackage()).thenReturn(true);
249         when(packageForFirstUser.isInstalledPackage()).thenReturn(false);
250         when(packageForFirstUser.getUserInfo()).thenReturn(mFirstUser);
251 
252         WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class);
253         when(wvusWrapper.getPackageInfosAllUsers(any(), eq(PACKAGE_NAME)))
254                 .thenReturn(Collections.singletonList(packageForFirstUser));
255 
256         final String expectedReason = String.format("(uninstalled for user %s)", mFirstUser.name);
257         assertThat(mPicker.getDisabledReason(wvusWrapper, mContext, PACKAGE_NAME))
258                 .isEqualTo(expectedReason);
259     }
260 
261     @Test
testDisabledReasonSeveralUsers()262     public void testDisabledReasonSeveralUsers() {
263         UserPackage packageForFirstUser = mock(UserPackage.class);
264         when(packageForFirstUser.isEnabledPackage()).thenReturn(false);
265         when(packageForFirstUser.isInstalledPackage()).thenReturn(true);
266         when(packageForFirstUser.getUserInfo()).thenReturn(mFirstUser);
267 
268         UserPackage packageForSecondUser = mock(UserPackage.class);
269         when(packageForSecondUser.isEnabledPackage()).thenReturn(true);
270         when(packageForSecondUser.isInstalledPackage()).thenReturn(false);
271         when(packageForSecondUser.getUserInfo()).thenReturn(mSecondUser);
272 
273         WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class);
274         when(wvusWrapper.getPackageInfosAllUsers(any(), eq(PACKAGE_NAME)))
275                 .thenReturn(Arrays.asList(packageForFirstUser, packageForSecondUser));
276 
277         final String expectedReason = String.format("(disabled for user %s)", mFirstUser.name);
278         assertThat(mPicker.getDisabledReason(wvusWrapper, mContext, PACKAGE_NAME))
279                 .isEqualTo(expectedReason);
280     }
281 
282     /**
283      * Ensure we only proclaim a package as uninstalled for a certain user if it's both uninstalled
284      * and disabled.
285      */
286     @Test
testDisabledReasonUninstalledAndDisabled()287     public void testDisabledReasonUninstalledAndDisabled() {
288         UserPackage packageForFirstUser = mock(UserPackage.class);
289         when(packageForFirstUser.isEnabledPackage()).thenReturn(false);
290         when(packageForFirstUser.isInstalledPackage()).thenReturn(false);
291         when(packageForFirstUser.getUserInfo()).thenReturn(mFirstUser);
292 
293         UserPackage packageForSecondUser = mock(UserPackage.class);
294         when(packageForSecondUser.isEnabledPackage()).thenReturn(true);
295         when(packageForSecondUser.isInstalledPackage()).thenReturn(true);
296         when(packageForSecondUser.getUserInfo()).thenReturn(mSecondUser);
297 
298         WebViewUpdateServiceWrapper wvusWrapper = mock(WebViewUpdateServiceWrapper.class);
299         when(wvusWrapper.getPackageInfosAllUsers(any(), eq(PACKAGE_NAME)))
300                 .thenReturn(Arrays.asList(packageForFirstUser, packageForSecondUser));
301 
302         final String expectedReason = String.format("(uninstalled for user %s)", mFirstUser.name);
303         assertThat(mPicker.getDisabledReason(wvusWrapper, mContext, PACKAGE_NAME))
304                 .isEqualTo(expectedReason);
305     }
306 
307     /**
308      * Ensure that the version name of a WebView package is displayed after its name in the
309      * preference title.
310      */
311     @Test
testWebViewVersionAddedAfterLabel()312     public void testWebViewVersionAddedAfterLabel() {
313         final DefaultAppInfo webviewAppInfo = mPicker.createDefaultAppInfo(mContext,
314                 mContext.getPackageManager(),
315                 createApplicationInfo(PACKAGE_NAME), "" /* disabledReason */);
316 
317         final RadioButtonPreference mockPreference = mock(RadioButtonPreference.class);
318         mPicker.bindPreference(mockPreference, PACKAGE_NAME, webviewAppInfo, null);
319         mPicker.bindPreferenceExtra(
320                 mockPreference, PACKAGE_NAME, webviewAppInfo, null, null);
321 
322         verify(mockPreference).setTitle(eq(PACKAGE_NAME + " " + PACKAGE_VERSION));
323         verify(mockPreference).setTitle(any());
324     }
325 
createApplicationInfo(String packageName)326     private static ApplicationInfo createApplicationInfo(String packageName) {
327         ApplicationInfo ai = new ApplicationInfo();
328         ai.packageName = packageName;
329         return ai;
330     }
331 
useWebViewSettingIntent()332     private void useWebViewSettingIntent() {
333         Intent intent = new Intent(ACTION_WEBVIEW_SETTINGS);
334         when(mActivity.getIntent()).thenReturn(intent);
335     }
336 
testSuccessfulClickChangesProvider()337     private void testSuccessfulClickChangesProvider() {
338         when(mWvusWrapper.getValidWebViewApplicationInfos(any()))
339                 .thenReturn(Collections.singletonList(createApplicationInfo(PACKAGE_NAME)));
340         when(mWvusWrapper.setWebViewProvider(eq(PACKAGE_NAME))).thenReturn(true);
341 
342         RadioButtonPreference defaultPackagePref = mock(RadioButtonPreference.class);
343         when(defaultPackagePref.getKey()).thenReturn(PACKAGE_NAME);
344         mPicker.onRadioButtonClicked(defaultPackagePref);
345 
346         verify(mWvusWrapper, times(1)).setWebViewProvider(eq(PACKAGE_NAME));
347         verify(mPicker, times(1)).updateCheckedState(PACKAGE_NAME);
348         verify(mWvusWrapper, never()).showInvalidChoiceToast(any());
349     }
350 
testFailingClickUpdatesSetting()351     private void testFailingClickUpdatesSetting() {
352         when(mWvusWrapper.getValidWebViewApplicationInfos(any()))
353                 .thenReturn(Collections.singletonList(createApplicationInfo(PACKAGE_NAME)));
354         when(mWvusWrapper.setWebViewProvider(eq(PACKAGE_NAME))).thenReturn(false);
355 
356         RadioButtonPreference defaultPackagePref = mock(RadioButtonPreference.class);
357         when(defaultPackagePref.getKey()).thenReturn(PACKAGE_NAME);
358         mPicker.onRadioButtonClicked(defaultPackagePref);
359 
360         verify(mWvusWrapper, times(1)).setWebViewProvider(eq(PACKAGE_NAME));
361         // Ensure we update the list of packages when we click a non-valid package - the list must
362         // have changed, otherwise this click wouldn't fail.
363         verify(mPicker, times(1)).updateCandidates();
364         verify(mWvusWrapper, times(1)).showInvalidChoiceToast(any());
365     }
366 }
367