1 /*
2  * Copyright (C) 2021 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.car.carlauncher;
18 
19 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
20 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
21 import static android.content.pm.PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS;
22 
23 import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_LAUNCHABLES;
24 import static com.android.car.carlauncher.AppLauncherUtils.APP_TYPE_MEDIA_SERVICES;
25 
26 import static org.junit.Assert.assertEquals;
27 import static org.junit.Assert.assertTrue;
28 import static org.junit.Assert.fail;
29 import static org.mockito.ArgumentMatchers.any;
30 import static org.mockito.ArgumentMatchers.anyInt;
31 import static org.mockito.ArgumentMatchers.anyString;
32 import static org.mockito.ArgumentMatchers.eq;
33 import static org.mockito.Mockito.never;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 
38 import android.car.Car;
39 import android.car.content.pm.CarPackageManager;
40 import android.car.media.CarMediaManager;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.pm.ActivityInfo;
44 import android.content.pm.ApplicationInfo;
45 import android.content.pm.LauncherApps;
46 import android.content.pm.PackageManager;
47 import android.content.pm.ResolveInfo;
48 import android.content.pm.ServiceInfo;
49 import android.service.media.MediaBrowserService;
50 import android.util.ArraySet;
51 
52 import androidx.test.ext.junit.runners.AndroidJUnit4;
53 import androidx.test.filters.SmallTest;
54 import androidx.test.platform.app.InstrumentationRegistry;
55 
56 import org.junit.Before;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 import org.mockito.Mock;
60 import org.mockito.MockitoAnnotations;
61 import org.xmlpull.v1.XmlPullParser;
62 import org.xmlpull.v1.XmlPullParserException;
63 import org.xmlpull.v1.XmlPullParserFactory;
64 
65 import java.io.StringReader;
66 import java.util.ArrayList;
67 import java.util.Arrays;
68 import java.util.Collections;
69 import java.util.List;
70 import java.util.function.Consumer;
71 import java.util.function.Predicate;
72 
73 @RunWith(AndroidJUnit4.class)
74 @SmallTest
75 public final class AppLauncherUtilsTest {
76     private static final String TEST_DISABLED_APP_1 = "com.android.car.test.disabled1";
77     private static final String TEST_DISABLED_APP_2 = "com.android.car.test.disabled2";
78     private static final String TEST_ENABLED_APP = "com.android.car.test.enabled";
79     private static final String TEST_VIDEO_APP = "com.android.car.test.video";
80 
81     private static final Predicate<ResolveInfo> MATCH_NO_APP = (resolveInfo) -> false;
82 
83     @Mock
84     private Context mMockContext;
85     @Mock
86     private LauncherApps mMockLauncherApps;
87     @Mock
88     private PackageManager mMockPackageManager;
89 
90     private CarMediaManager mCarMediaManager;
91     private CarPackageManager mCarPackageManager;
92 
93     private XmlPullParserFactory mParserFactory;
94 
95     @Before
setUp()96     public void setUp() throws XmlPullParserException {
97         MockitoAnnotations.initMocks(this);
98         Car car = Car.createCar(mMockContext);
99         mCarPackageManager = (CarPackageManager) car.getCarManager(Car.PACKAGE_SERVICE);
100         mCarMediaManager = (CarMediaManager) car.getCarManager(Car.CAR_MEDIA_SERVICE);
101 
102         mParserFactory = XmlPullParserFactory.newInstance();
103         mParserFactory.setNamespaceAware(true);
104     }
105 
106     @Test
testGetLauncherAppsWithEnableAndLaunchDisabledApps()107     public void testGetLauncherAppsWithEnableAndLaunchDisabledApps() {
108         mockPackageManagerQueries();
109         injectApplicationEnabledSetting(COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
110 
111         AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
112                 /* appsToHide= */ new ArraySet<>(), /* customMediaComponents= */ new ArraySet<>(),
113                 /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
114                 /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
115                 mMockPackageManager, MATCH_NO_APP, mCarMediaManager);
116 
117         List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
118 
119         // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
120         assertEquals(3, appMetaData.size());
121 
122         injectApplicationEnabledSetting(COMPONENT_ENABLED_STATE_ENABLED);
123         launchAllApps(appMetaData);
124 
125         verify(mMockPackageManager).setApplicationEnabledSetting(
126                 eq(TEST_DISABLED_APP_1), eq(COMPONENT_ENABLED_STATE_ENABLED), eq(0));
127         verify(mMockPackageManager, times(2)).getApplicationEnabledSetting(eq(TEST_DISABLED_APP_1));
128 
129         verify(mMockPackageManager).setApplicationEnabledSetting(
130                 eq(TEST_DISABLED_APP_2), eq(COMPONENT_ENABLED_STATE_ENABLED), eq(0));
131         verify(mMockPackageManager, times(2)).getApplicationEnabledSetting(eq(TEST_DISABLED_APP_2));
132 
133         verify(mMockContext, times(2)).startActivity(any(), any());
134     }
135 
136     @Test
testGetLauncherAppsWithNotEnablingEnabledApps()137     public void testGetLauncherAppsWithNotEnablingEnabledApps() {
138         mockPackageManagerQueries();
139         injectApplicationEnabledSetting(COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED);
140 
141         AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
142                 /* appsToHide= */ new ArraySet<>(), /* customMediaComponents= */ new ArraySet<>(),
143                 /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
144                 /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
145                 mMockPackageManager, MATCH_NO_APP, mCarMediaManager);
146 
147         List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
148 
149         // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
150         assertEquals(3, appMetaData.size());
151 
152         injectApplicationEnabledSetting(COMPONENT_ENABLED_STATE_ENABLED);
153         launchAllApps(appMetaData);
154 
155         verify(mMockPackageManager, never()).setApplicationEnabledSetting(
156                 eq(TEST_ENABLED_APP), anyInt(), eq(0));
157         verify(mMockPackageManager, never()).getApplicationEnabledSetting(eq(TEST_ENABLED_APP));
158     }
159 
160     @Test
testGetLauncherAppsWithEnableAndVideoApps()161     public void testGetLauncherAppsWithEnableAndVideoApps() {
162         mockPackageManagerQueriesForVideo();
163         injectApplicationEnabledSetting(COMPONENT_ENABLED_STATE_ENABLED);
164 
165         AppLauncherUtils.LauncherAppsInfo launcherAppsInfo = AppLauncherUtils.getLauncherApps(
166                 /* appsToHide= */ new ArraySet<>(), /* customMediaComponents= */ new ArraySet<>(),
167                 /* appTypes= */ APP_TYPE_LAUNCHABLES + APP_TYPE_MEDIA_SERVICES,
168                 /* openMediaCenter= */ false, mMockLauncherApps, mCarPackageManager,
169                 mMockPackageManager, new TestVideoAppPredicate(), mCarMediaManager);
170 
171         // mMockLauncherApps is never stubbed, only services & disabled activities are expected.
172         List<AppMetaData> appMetaData = launcherAppsInfo.getLaunchableComponentsList();
173 
174         // TEST_VIDEO_APP should be filtered by TestVideoAppPredicate above.
175         assertEquals(1, appMetaData.size());
176         assertEquals(TEST_ENABLED_APP, appMetaData.get(0).getPackageName());
177     }
178 
179 
180     @Test
getAutomotiveAppTypes()181     public void getAutomotiveAppTypes() {
182         // This test relies on test app's manifest & xml resources.
183         Context testContext =
184                 InstrumentationRegistry.getInstrumentation().getContext();
185         assertEquals(
186                 Arrays.asList("video", "media"),
187                 AppLauncherUtils.getAutomotiveAppTypes(
188                         testContext.getPackageManager(),
189                         testContext.getPackageName()));
190     }
191 
192     @Test
videoAppPredicate()193     public void videoAppPredicate() {
194         // This test relies on test app's manifest & xml resources.
195         Context testContext =
196                 InstrumentationRegistry.getInstrumentation().getContext();
197         Predicate<ResolveInfo> predicate =
198                 new AppLauncherUtils.VideoAppPredicate(testContext.getPackageManager());
199 
200         assertTrue(predicate.test(constructServiceResolveInfo(testContext.getPackageName())));
201     }
202 
203     @Test
invalidAutomotiveXml()204     public void invalidAutomotiveXml() {
205         StringBuilder hugeInvalidXml = new StringBuilder("<automotiveApp>");
206         for (int i = 0; i < 65; i++) {
207             hugeInvalidXml.append("<uses name=\"video\"/>");
208         }
209         hugeInvalidXml.append("</automotiveApp>");
210 
211         String[] invalidXmls = {
212                 "NoTagsHere",
213                 // Unknown tag.
214                 "<foo/>",
215                 // Manifest tag not expected.
216                 "<automotiveApp><manifest/></automotiveApp>",
217                 // Uses tag has missing name attribute.
218                 "<automotiveApp><uses/></automotiveApp>",
219                 // Uses tag has empty name attribute.
220                 "<automotiveApp><uses name=\"\"/></automotiveApp>",
221                 // Uses tag nested inside uses tag.
222                 "<automotiveApp><uses name=\"video\"><uses name=\"media\"/></uses></automotiveApp>",
223                 // Too many uses tags
224                 hugeInvalidXml.toString()
225         };
226 
227         for (String invalidXml : invalidXmls) {
228             List<String> appTypes =
229                     AppLauncherUtils.parseAutomotiveAppTypes(createPullParser(invalidXml));
230             assertEquals(0, appTypes.size());
231         }
232     }
233 
createPullParser(String xmlText)234     private XmlPullParser createPullParser(String xmlText) {
235         try {
236             XmlPullParser parser = mParserFactory.newPullParser();
237             parser.setInput(new StringReader(xmlText));
238             return parser;
239         } catch (XmlPullParserException e) {
240             fail("Unexpected failure");
241             return null;
242         }
243     }
244 
mockPackageManagerQueriesForVideo()245     private void mockPackageManagerQueriesForVideo() {
246         when(mMockPackageManager.queryIntentServices(any(), anyInt())).thenAnswer(args -> {
247             Intent intent = args.getArgument(0);
248             if (intent.getAction().equals(MediaBrowserService.SERVICE_INTERFACE)) {
249                 return Arrays.asList(
250                         constructServiceResolveInfo(TEST_ENABLED_APP),
251                         constructServiceResolveInfo(TEST_VIDEO_APP));
252             }
253             return new ArrayList<>();
254         });
255     }
256 
mockPackageManagerQueries()257     private void mockPackageManagerQueries() {
258         when(mMockPackageManager.queryIntentServices(any(), anyInt())).thenAnswer(args -> {
259             Intent intent = args.getArgument(0);
260             if (intent.getAction().equals(MediaBrowserService.SERVICE_INTERFACE)) {
261                 return Collections.singletonList(constructServiceResolveInfo(TEST_ENABLED_APP));
262             }
263             return new ArrayList<>();
264         });
265         when(mMockPackageManager.queryIntentActivities(any(), anyInt())).thenAnswer(args -> {
266             Intent intent = args.getArgument(0);
267             int flags = args.getArgument(1);
268             List<ResolveInfo> resolveInfoList = new ArrayList<>();
269             if (intent.getAction().equals(Intent.ACTION_MAIN)) {
270                 if ((flags & MATCH_DISABLED_UNTIL_USED_COMPONENTS) != 0) {
271                     resolveInfoList.add(constructActivityResolveInfo(TEST_DISABLED_APP_1));
272                     resolveInfoList.add(constructActivityResolveInfo(TEST_DISABLED_APP_2));
273                 }
274                 resolveInfoList.add(constructActivityResolveInfo(TEST_ENABLED_APP));
275             }
276             return resolveInfoList;
277         });
278     }
279 
injectApplicationEnabledSetting(int enabledState)280     private void injectApplicationEnabledSetting(int enabledState) {
281         when(mMockPackageManager.getApplicationEnabledSetting(anyString()))
282                 .thenReturn(enabledState);
283     }
284 
launchAllApps(List<AppMetaData> appMetaData)285     private void launchAllApps(List<AppMetaData> appMetaData) {
286         for (AppMetaData meta : appMetaData) {
287             Consumer<Context> launchCallback = meta.getLaunchCallback();
288             launchCallback.accept(mMockContext);
289         }
290     }
291 
constructActivityResolveInfo(String packageName)292     private static ResolveInfo constructActivityResolveInfo(String packageName) {
293         ResolveInfo info = new ResolveInfo();
294         info.activityInfo = new ActivityInfo();
295         info.activityInfo.packageName = packageName;
296         info.activityInfo.name = packageName + ".activity";
297         info.activityInfo.applicationInfo = new ApplicationInfo();
298         return info;
299     }
300 
constructServiceResolveInfo(String packageName)301     private static ResolveInfo constructServiceResolveInfo(String packageName) {
302         ResolveInfo info = new ResolveInfo();
303         info.serviceInfo = new ServiceInfo();
304         info.serviceInfo.packageName = packageName;
305         info.serviceInfo.name = packageName + ".service";
306         info.serviceInfo.applicationInfo = new ApplicationInfo();
307         return info;
308     }
309 
310     /** Test sub-class of VideoAppPredicate that only matches TEST_VIDEO_APP package-name */
311     static class TestVideoAppPredicate extends AppLauncherUtils.VideoAppPredicate {
TestVideoAppPredicate()312         TestVideoAppPredicate() {
313             super(/* packageManager= */ null);
314         }
315 
316         @Override
test(ResolveInfo resolveInfo)317         public boolean test(ResolveInfo resolveInfo) {
318             return TEST_VIDEO_APP.equals(super.getPackageName(resolveInfo));
319         }
320     }
321 }
322