1 /*
2  * Copyright 2023 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 android.virtualdevice.cts.applaunch;
18 
19 import static android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE;
20 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
21 import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
22 import static android.content.pm.PackageManager.DONT_KILL_APP;
23 import static android.content.pm.PackageManager.FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS;
24 import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
25 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
26 import static android.virtualdevice.cts.common.StreamedAppConstants.CUSTOM_HOME_ACTIVITY;
27 import static android.virtualdevice.cts.common.StreamedAppConstants.DEFAULT_HOME_ACTIVITY;
28 
29 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
30 
31 import static com.google.common.truth.Truth.assertThat;
32 
33 import static org.junit.Assume.assumeNotNull;
34 import static org.junit.Assume.assumeTrue;
35 import static org.mockito.ArgumentMatchers.any;
36 import static org.mockito.ArgumentMatchers.anyInt;
37 import static org.mockito.ArgumentMatchers.eq;
38 import static org.mockito.Mockito.never;
39 import static org.mockito.Mockito.reset;
40 import static org.mockito.Mockito.timeout;
41 import static org.mockito.Mockito.verify;
42 
43 import android.app.WallpaperManager;
44 import android.companion.virtual.VirtualDeviceManager;
45 import android.companion.virtual.VirtualDeviceParams;
46 import android.companion.virtual.flags.Flags;
47 import android.content.ComponentName;
48 import android.content.Context;
49 import android.content.Intent;
50 import android.content.pm.ActivityInfo;
51 import android.content.pm.PackageManager;
52 import android.content.pm.ResolveInfo;
53 import android.content.res.Resources;
54 import android.hardware.display.DisplayManager;
55 import android.hardware.display.VirtualDisplay;
56 import android.hardware.display.VirtualDisplayConfig;
57 import android.platform.test.annotations.AppModeFull;
58 import android.platform.test.annotations.RequiresFlagsEnabled;
59 import android.server.wm.WindowManagerState;
60 import android.virtualdevice.cts.applaunch.AppComponents.EmptyActivity;
61 import android.virtualdevice.cts.common.VirtualDeviceRule;
62 
63 import androidx.test.ext.junit.runners.AndroidJUnit4;
64 
65 import com.android.compatibility.common.util.ApiTest;
66 import com.android.compatibility.common.util.FeatureUtil;
67 import com.android.compatibility.common.util.SystemUtil;
68 
69 import org.junit.Before;
70 import org.junit.Rule;
71 import org.junit.Test;
72 import org.junit.runner.RunWith;
73 import org.mockito.Mock;
74 import org.mockito.MockitoAnnotations;
75 
76 import java.util.concurrent.TimeUnit;
77 
78 /**
79  * Tests for home support on displays created by virtual devices.
80  */
81 @RunWith(AndroidJUnit4.class)
82 @AppModeFull(reason = "VirtualDeviceManager cannot be accessed by instant apps")
83 @RequiresFlagsEnabled(Flags.FLAG_VDM_CUSTOM_HOME)
84 public class VirtualDeviceHomeTest {
85 
86     private static final VirtualDisplayConfig.Builder HOME_DISPLAY_CONFIG =
87             VirtualDeviceRule.createDefaultVirtualDisplayConfigBuilder()
88                     .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
89                             | DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
90                             | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
91     private static final VirtualDisplayConfig.Builder UNTRUSTED_HOME_DISPLAY_CONFIG =
92             VirtualDeviceRule.createDefaultVirtualDisplayConfigBuilder()
93                     .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
94                             | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
95     private static final VirtualDisplayConfig.Builder AUTO_MIRROR_HOME_DISPLAY_CONFIG =
96             VirtualDeviceRule.createDefaultVirtualDisplayConfigBuilder()
97                     .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR);
98     private static final VirtualDisplayConfig.Builder PUBLIC_HOME_DISPLAY_CONFIG =
99             VirtualDeviceRule.createDefaultVirtualDisplayConfigBuilder()
100                     .setFlags(DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC);
101 
102     private static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(3);
103 
104     @Rule
105     public VirtualDeviceRule mRule = VirtualDeviceRule.withAdditionalPermissions(
106             CHANGE_COMPONENT_ENABLED_STATE);
107 
108     private final Context mContext = getInstrumentation().getContext();
109 
110     private VirtualDisplay mVirtualDisplay;
111 
112     @Mock
113     private VirtualDeviceManager.ActivityListener mActivityListener;
114 
115     @Before
setUp()116     public void setUp() {
117         MockitoAnnotations.initMocks(this);
118         assumeTrue(FeatureUtil.hasSystemFeature(FEATURE_ACTIVITIES_ON_SECONDARY_DISPLAYS));
119         assumeTrue(isHomeSupportedOnVirtualDisplay());
120     }
121 
122     /**
123      * Home activities and wallpaper are not shown on untrusted displays.
124      */
125     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
126     @Test
virtualDeviceHome_untrustedVirtualDisplay()127     public void virtualDeviceHome_untrustedVirtualDisplay() {
128         createVirtualDeviceAndHomeDisplay(UNTRUSTED_HOME_DISPLAY_CONFIG, /* homeComponent= */ null);
129 
130         verify(mActivityListener, never()).onTopActivityChanged(anyInt(), any(), anyInt());
131         assertThat(isWallpaperOnVirtualDisplay(mRule.getWmState())).isFalse();
132     }
133 
134     /**
135      * Home activities and wallpaper are not shown on auto-mirror displays.
136      */
137     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
138     @Test
virtualDeviceHome_autoMirrorVirtualDisplay()139     public void virtualDeviceHome_autoMirrorVirtualDisplay() {
140         createVirtualDeviceAndHomeDisplay(AUTO_MIRROR_HOME_DISPLAY_CONFIG,
141                 /* homeComponent= */ null);
142 
143         verify(mActivityListener, never()).onTopActivityChanged(anyInt(), any(), anyInt());
144         assertThat(isWallpaperOnVirtualDisplay(mRule.getWmState())).isFalse();
145     }
146 
147     /**
148      * Home activities and wallpaper are not shown on public displays without the flag
149      * VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY.
150      */
151     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
152     @Test
virtualDeviceHome_publicVirtualDisplay()153     public void virtualDeviceHome_publicVirtualDisplay() {
154         createVirtualDeviceAndHomeDisplay(PUBLIC_HOME_DISPLAY_CONFIG, /* homeComponent= */ null);
155 
156         verify(mActivityListener, never()).onTopActivityChanged(anyInt(), any(), anyInt());
157         assertThat(isWallpaperOnVirtualDisplay(mRule.getWmState())).isFalse();
158     }
159 
160     /**
161      * Wallpaper is shown on virtual displays that support home without a custom home component.
162      */
163     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
164     @Test
virtualDeviceHome_noHomeComponent_showsWallpaper()165     public void virtualDeviceHome_noHomeComponent_showsWallpaper() {
166         assumeTrue(WallpaperManager.getInstance(mContext).isWallpaperSupported());
167         try (HomeActivitySession ignored = new HomeActivitySession(DEFAULT_HOME_ACTIVITY)) {
168             createVirtualDeviceAndHomeDisplay(/* homeComponent= */ null);
169             assertThat(mRule.getWmState().waitForWithAmState(
170                     this::isWallpaperOnVirtualDisplay, "Wallpaper is on virtual display"))
171                     .isTrue();
172         }
173     }
174 
175     /**
176      * Wallpaper is shown on virtual displays that support home with a custom home component.
177      */
178     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
179     @Test
virtualDeviceHome_withHomeComponent_showsWallpaper()180     public void virtualDeviceHome_withHomeComponent_showsWallpaper() {
181         assumeTrue(WallpaperManager.getInstance(mContext).isWallpaperSupported());
182         createVirtualDeviceAndHomeDisplay(CUSTOM_HOME_ACTIVITY);
183         assertThat(mRule.getWmState().waitForWithAmState(
184                 this::isWallpaperOnVirtualDisplay, "Wallpaper is on virtual display"))
185                 .isTrue();
186     }
187 
188     /**
189      * The device-default secondary home activity is started on virtual displays that support home
190      * if the didn't specify a custom home component. That activity is at the hierarchy root.
191      */
192     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
193     @Test
virtualDeviceHome_noCustomHomeComponent()194     public void virtualDeviceHome_noCustomHomeComponent() {
195         try (HomeActivitySession session = new HomeActivitySession(DEFAULT_HOME_ACTIVITY)) {
196             final ComponentName homeComponent = session.getCurrentSecondaryHomeComponent();
197             createVirtualDeviceAndHomeDisplay(/* homeComponent= */ null);
198             assertActivityOnVirtualDisplay(homeComponent);
199 
200             EmptyActivity activity =
201                     mRule.startActivityOnDisplaySync(mVirtualDisplay, EmptyActivity.class);
202             assertActivityOnVirtualDisplay(activity.getComponentName());
203 
204             activity.finish();
205             mRule.getWmState().waitAndAssertActivityRemoved(activity.getComponentName());
206 
207             assertActivityOnVirtualDisplay(homeComponent);
208         }
209     }
210 
211     /**
212      * The device-default secondary home activity is started on virtual displays that support home
213      * if the didn't specify a custom home component. That activity is resolved when a home intent
214      * is sent to the relevant display.
215      */
216     @ApiTest(apis = {"android.hardware.display.VirtualDisplayConfig.Builder#setHomeSupported"})
217     @Test
virtualDeviceHome_noCustomHomeComponent_sendHomeIntent()218     public void virtualDeviceHome_noCustomHomeComponent_sendHomeIntent() {
219         try (HomeActivitySession session = new HomeActivitySession(DEFAULT_HOME_ACTIVITY)) {
220             final ComponentName homeComponent = session.getCurrentSecondaryHomeComponent();
221             createVirtualDeviceAndHomeDisplay(/* homeComponent= */ null);
222             assertActivityOnVirtualDisplay(homeComponent);
223 
224             EmptyActivity activity =
225                     mRule.startActivityOnDisplaySync(mVirtualDisplay, EmptyActivity.class);
226             assertActivityOnVirtualDisplay(activity.getComponentName());
227 
228             sendHomeIntentOnVirtualDisplay();
229             assertActivityOnVirtualDisplay(homeComponent);
230         }
231     }
232 
233     /**
234      * The device-default secondary home activity is started on virtual displays that support home
235      * if they specified an invalid custom home component.
236      */
237     @ApiTest(apis = {"android.companion.virtual.VirtualDeviceParams.Builder#setHomeComponent"})
238     @Test
virtualDeviceHome_invalidCustomHomeComponent_fallbackToDefaultSecondaryHome()239     public void virtualDeviceHome_invalidCustomHomeComponent_fallbackToDefaultSecondaryHome() {
240         try (HomeActivitySession session = new HomeActivitySession(DEFAULT_HOME_ACTIVITY)) {
241             final ComponentName homeComponent = session.getCurrentSecondaryHomeComponent();
242             createVirtualDeviceAndHomeDisplay(new ComponentName("foo.bar", "foo.bar.Baz"));
243             assertActivityOnVirtualDisplay(homeComponent);
244         }
245     }
246 
247     /**
248      * The explicitly specified custom home activity is started on virtual displays that support
249      * home. That activity is at the hierarchy root.
250      */
251     @ApiTest(apis = {"android.companion.virtual.VirtualDeviceParams.Builder#setHomeComponent"})
252     @Test
virtualDeviceHome_withCustomHomeComponent()253     public void virtualDeviceHome_withCustomHomeComponent() {
254         createVirtualDeviceAndHomeDisplay(CUSTOM_HOME_ACTIVITY);
255         assertActivityOnVirtualDisplay(CUSTOM_HOME_ACTIVITY);
256 
257         EmptyActivity activity =
258                 mRule.startActivityOnDisplaySync(mVirtualDisplay, EmptyActivity.class);
259         assertActivityOnVirtualDisplay(activity.getComponentName());
260 
261         activity.finish();
262         mRule.getWmState().waitAndAssertActivityRemoved(activity.getComponentName());
263 
264         assertActivityOnVirtualDisplay(CUSTOM_HOME_ACTIVITY);
265     }
266 
267     /**
268      * The explicitly specified custom home activity is started on virtual displays that support
269      * home. That activity is resolved when a home intent is sent to the relevant display.
270      */
271     @ApiTest(apis = {"android.companion.virtual.VirtualDeviceParams.Builder#setHomeComponent"})
272     @Test
virtualDeviceHome_withCustomHomeComponent_sendHomeIntent()273     public void virtualDeviceHome_withCustomHomeComponent_sendHomeIntent() {
274         createVirtualDeviceAndHomeDisplay(CUSTOM_HOME_ACTIVITY);
275         assertActivityOnVirtualDisplay(CUSTOM_HOME_ACTIVITY);
276 
277         EmptyActivity activity =
278                 mRule.startActivityOnDisplaySync(mVirtualDisplay, EmptyActivity.class);
279         assertActivityOnVirtualDisplay(activity.getComponentName());
280 
281         sendHomeIntentOnVirtualDisplay();
282         assertActivityOnVirtualDisplay(CUSTOM_HOME_ACTIVITY);
283     }
284 
createVirtualDeviceAndHomeDisplay(ComponentName homeComponent)285     private void createVirtualDeviceAndHomeDisplay(ComponentName homeComponent) {
286         createVirtualDeviceAndHomeDisplay(HOME_DISPLAY_CONFIG, homeComponent);
287     }
288 
createVirtualDeviceAndHomeDisplay( VirtualDisplayConfig.Builder virtualDisplayConfigBuilder, ComponentName homeComponent)289     private void createVirtualDeviceAndHomeDisplay(
290             VirtualDisplayConfig.Builder virtualDisplayConfigBuilder, ComponentName homeComponent) {
291         VirtualDeviceManager.VirtualDevice virtualDevice = mRule.createManagedVirtualDevice(
292                 new VirtualDeviceParams.Builder().setHomeComponent(homeComponent).build());
293         virtualDevice.addActivityListener(mContext.getMainExecutor(), mActivityListener);
294         mVirtualDisplay = mRule.createManagedVirtualDisplay(
295                 virtualDevice, virtualDisplayConfigBuilder.setHomeSupported(true).build());
296     }
297 
sendHomeIntentOnVirtualDisplay()298     private void sendHomeIntentOnVirtualDisplay() {
299         Intent intent = new Intent(Intent.ACTION_MAIN);
300         intent.addCategory(Intent.CATEGORY_HOME);
301         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
302         mRule.sendIntentToDisplay(intent, mVirtualDisplay);
303     }
304 
assertActivityOnVirtualDisplay(ComponentName componentName)305     private void assertActivityOnVirtualDisplay(ComponentName componentName) {
306         verify(mActivityListener, timeout(TIMEOUT_MILLIS)).onTopActivityChanged(
307                 eq(mVirtualDisplay.getDisplay().getDisplayId()), eq(componentName),
308                 eq(mContext.getUserId()));
309         reset(mActivityListener);
310     }
311 
isWallpaperOnVirtualDisplay(WindowManagerState state)312     private boolean isWallpaperOnVirtualDisplay(WindowManagerState state) {
313         return state.getMatchingWindowType(TYPE_WALLPAPER).stream().anyMatch(
314                 w -> w.getDisplayId() == mVirtualDisplay.getDisplay().getDisplayId());
315     }
316 
isHomeSupportedOnVirtualDisplay()317     private boolean isHomeSupportedOnVirtualDisplay() {
318         try {
319             return mContext.getResources().getBoolean(
320                     Resources.getSystem().getIdentifier(
321                             "config_supportsSystemDecorsOnSecondaryDisplays", "bool", "android"));
322         } catch (Resources.NotFoundException e) {
323             // Assume this device support system decorations.
324             return true;
325         }
326     }
327 
328     /**
329      * HomeActivitySession is used to replace the default home component, so that you can use
330      * your preferred home for testing within the session. The original default home will be
331      * restored automatically afterward.
332      */
333     private class HomeActivitySession implements AutoCloseable {
334         private final PackageManager mPackageManager;
335         private final String mOrigHome;
336         private final ComponentName mSessionHome;
337 
HomeActivitySession(ComponentName sessionHome)338         HomeActivitySession(ComponentName sessionHome) {
339             mSessionHome = sessionHome;
340             mPackageManager = mContext.getPackageManager();
341             mOrigHome = getDefaultHomeComponent();
342 
343             mPackageManager.setComponentEnabledSetting(mSessionHome,
344                     COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP);
345             setDefaultHome(mSessionHome);
346         }
347 
348         @Override
close()349         public void close() {
350             mPackageManager.setComponentEnabledSetting(mSessionHome,
351                     COMPONENT_ENABLED_STATE_DISABLED, DONT_KILL_APP);
352             if (mOrigHome != null) {
353                 setDefaultHome(mOrigHome);
354             }
355         }
356 
setDefaultHome(ComponentName componentName)357         private void setDefaultHome(ComponentName componentName) {
358             setDefaultHome(componentName.flattenToString());
359         }
360 
setDefaultHome(String component)361         private void setDefaultHome(String component) {
362             SystemUtil.runShellCommand("cmd package set-home-activity --user "
363                     + android.os.Process.myUserHandle().getIdentifier() + " "
364                     + component);
365         }
366 
getCurrentSecondaryHomeComponent()367         ComponentName getCurrentSecondaryHomeComponent() {
368             boolean useSystemProvidedLauncher = mContext.getResources().getBoolean(
369                     Resources.getSystem().getIdentifier(
370                             "config_useSystemProvidedLauncherForSecondary",
371                             "bool", "android"));
372             return useSystemProvidedLauncher ? getDefaultSecondaryHomeComponent() : mSessionHome;
373         }
374 
375         /** Fetches the component name of the default launcher. */
getDefaultHomeComponent()376         private String getDefaultHomeComponent() {
377             final String prefix = "Launcher: ComponentInfo{";
378             final String postfix = "}";
379             for (String s : SystemUtil.runShellCommand(
380                     "cmd shortcut get-default-launcher").split("\n")) {
381                 if (s.startsWith(prefix) && s.endsWith(postfix)) {
382                     return s.substring(prefix.length(), s.length() - postfix.length());
383                 }
384             }
385             throw new AssertionError("No default launcher found");
386         }
387 
getDefaultSecondaryHomeComponent()388         private ComponentName getDefaultSecondaryHomeComponent() {
389             int resId = Resources.getSystem().getIdentifier(
390                     "config_secondaryHomePackage", "string", "android");
391             final Intent intent = new Intent(Intent.ACTION_MAIN);
392             intent.addCategory(Intent.CATEGORY_SECONDARY_HOME);
393             intent.setPackage(mContext.getResources().getString(resId));
394             return resolveHomeIntent(intent);
395         }
396 
resolveHomeIntent(Intent intent)397         private ComponentName resolveHomeIntent(Intent intent) {
398             final ResolveInfo resolveInfo =
399                     mContext.getPackageManager().resolveActivity(intent, MATCH_DEFAULT_ONLY);
400             assumeNotNull(resolveInfo);
401             ActivityInfo activityInfo = resolveInfo.activityInfo;
402             return new ComponentName(activityInfo.packageName, activityInfo.name);
403         }
404     }
405 }
406