diff options
4 files changed, 178 insertions, 6 deletions
diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java index bb91a37825e6..760224f92acb 100644 --- a/core/java/android/content/pm/LauncherActivityInfo.java +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -213,6 +213,14 @@ public class LauncherActivityInfo { } /** + * Returns whether this activity supports (and can be launched in) multiple instances. + * @hide + */ + public boolean supportsMultiInstance() { + return mInternal.supportsMultiInstance(); + } + + /** * Check whether the {@code sequence} is visible to the user or not. * <p> * Return {@code false} when one of these conditions are satisfied: diff --git a/core/java/android/content/pm/LauncherActivityInfoInternal.java b/core/java/android/content/pm/LauncherActivityInfoInternal.java index 5aac97d784b3..3bb38799a211 100644 --- a/core/java/android/content/pm/LauncherActivityInfoInternal.java +++ b/core/java/android/content/pm/LauncherActivityInfoInternal.java @@ -32,19 +32,24 @@ public class LauncherActivityInfoInternal implements Parcelable { @NonNull private ComponentName mComponentName; @NonNull private IncrementalStatesInfo mIncrementalStatesInfo; @NonNull private UserHandle mUser; + private boolean mSupportsMultiInstance; /** * @param info ActivityInfo from which to create the LauncherActivityInfo. * @param incrementalStatesInfo The package's states. * @param user The user the activity info belongs to. + * @param supportsMultiInstance Whether the activity supports multi-instance as declared in its + * app manifest */ public LauncherActivityInfoInternal(@NonNull ActivityInfo info, @NonNull IncrementalStatesInfo incrementalStatesInfo, - @NonNull UserHandle user) { + @NonNull UserHandle user, + boolean supportsMultiInstance) { mActivityInfo = info; mComponentName = new ComponentName(info.packageName, info.name); mIncrementalStatesInfo = incrementalStatesInfo; mUser = user; + mSupportsMultiInstance = supportsMultiInstance; } public LauncherActivityInfoInternal(Parcel source) { @@ -52,6 +57,7 @@ public class LauncherActivityInfoInternal implements Parcelable { mComponentName = new ComponentName(mActivityInfo.packageName, mActivityInfo.name); mIncrementalStatesInfo = source.readTypedObject(IncrementalStatesInfo.CREATOR); mUser = source.readTypedObject(UserHandle.CREATOR); + mSupportsMultiInstance = source.readBoolean(); } public ComponentName getComponentName() { @@ -70,6 +76,10 @@ public class LauncherActivityInfoInternal implements Parcelable { return mIncrementalStatesInfo; } + public boolean supportsMultiInstance() { + return mSupportsMultiInstance; + } + @Override public int describeContents() { return 0; @@ -80,6 +90,7 @@ public class LauncherActivityInfoInternal implements Parcelable { dest.writeTypedObject(mActivityInfo, flags); dest.writeTypedObject(mIncrementalStatesInfo, flags); dest.writeTypedObject(mUser, flags); + dest.writeBoolean(mSupportsMultiInstance); } public static final @android.annotation.NonNull Creator<LauncherActivityInfoInternal> CREATOR = diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 4c959fafaa8d..2dd679818ada 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -35,6 +35,7 @@ import static android.content.PermissionChecker.checkCallingOrSelfPermissionForP import static android.content.pm.LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS; +import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; import static com.android.server.pm.PackageArchiver.isArchivingEnabled; @@ -118,7 +119,6 @@ import android.window.IDumpCallback; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; -import com.android.internal.infra.AndroidFuture; import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; @@ -850,7 +850,9 @@ public class LauncherAppsService extends SystemService { // package does not exist; should not happen return null; } - return new LauncherActivityInfoInternal(activityInfo, incrementalStatesInfo, user); + return new LauncherActivityInfoInternal(activityInfo, incrementalStatesInfo, user, + supportsMultiInstance(mIPM, activityInfo.getComponentName(), + user.getIdentifier())); } finally { Binder.restoreCallingIdentity(ident); } @@ -941,7 +943,7 @@ public class LauncherAppsService extends SystemService { archiveState.getActivityInfos(); for (int j = 0; j < archiveActivityInfoList.size(); j++) { launcherActivityList.add( - constructLauncherActivityInfoForArchivedApp( + constructLauncherActivityInfoForArchivedApp(mIPM, user, applicationInfo, archiveActivityInfoList.get(j))); } } @@ -949,6 +951,7 @@ public class LauncherAppsService extends SystemService { } private static LauncherActivityInfoInternal constructLauncherActivityInfoForArchivedApp( + IPackageManager pm, UserHandle user, ApplicationInfo applicationInfo, ArchiveState.ArchiveActivityInfo archiveActivityInfo) { @@ -964,7 +967,9 @@ public class LauncherAppsService extends SystemService { activityInfo, new IncrementalStatesInfo( false /* isLoading */, 0 /* progress */, 0 /* loadingCompletedTime */), - user); + user, + supportsMultiInstance(pm, activityInfo.getComponentName(), + user.getIdentifier())); } @NonNull @@ -1025,7 +1030,9 @@ public class LauncherAppsService extends SystemService { continue; } results.add(new LauncherActivityInfoInternal(ri.activityInfo, - incrementalStatesInfo, user)); + incrementalStatesInfo, user, + supportsMultiInstance(mIPM, ri.activityInfo.getComponentName(), + user.getIdentifier()))); } return results; } @@ -1660,6 +1667,29 @@ public class LauncherAppsService extends SystemService { } } + /** + * Returns whether the specified activity info has the multi-instance property declared. + */ + @VisibleForTesting + static boolean supportsMultiInstance(@NonNull IPackageManager pm, + @NonNull ComponentName component, int userId) { + try { + // Try to get the property for the component + return pm.getPropertyAsUser( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, component.getPackageName(), + component.getClassName(), userId).getBoolean(); + } catch (Exception e) { + try { + // Fallback to the property for the app + return pm.getPropertyAsUser( + PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, component.getPackageName(), + null, userId).getBoolean(); + } catch (Exception e2) { + return false; + } + } + } + @Override public @Nullable LauncherUserInfo getLauncherUserInfo(@NonNull UserHandle user) { if (!canAccessProfile(user.getIdentifier(), diff --git a/services/tests/servicestests/src/com/android/server/pm/LauncherAppsServiceTest.kt b/services/tests/servicestests/src/com/android/server/pm/LauncherAppsServiceTest.kt new file mode 100644 index 000000000000..aa1487b10a7b --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/LauncherAppsServiceTest.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm + +import android.app.ActivityTaskManager +import android.content.ComponentName +import android.content.pm.IPackageManager +import android.content.pm.PackageManager +import android.os.RemoteException +import android.platform.test.annotations.Postsubmit +import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.pm.LauncherAppsService.LauncherAppsImpl.supportsMultiInstance +import org.junit.Assert.assertEquals +import org.junit.Assume.assumeTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Unit tests for LauncherAppsService + * Run: atest LauncherAppsServiceTest + */ +@Postsubmit +@RunWith(AndroidJUnit4::class) +class LauncherAppsServiceTest { + + val pm = mock<IPackageManager>() + + @Before + fun setup() { + assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow( + InstrumentationRegistry.getInstrumentation().getTargetContext())) + } + + @Test + @Throws(RemoteException::class) + fun supportsMultiInstanceSplit_activityPropertyTrue() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + val activityProp = PackageManager.Property("", true, "", "") + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER))) + .thenReturn(activityProp) + val appProp = PackageManager.Property("", false, "", "") + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER))) + .thenReturn(appProp) + + // Expect activity property to override application property + assertEquals(true, supportsMultiInstance(pm, component, TEST_OTHER_USER)) + } + + @Test + @Throws(RemoteException::class) + fun supportsMultiInstanceSplit_activityPropertyFalseApplicationPropertyTrue() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + val activityProp = PackageManager.Property("", false, "", "") + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER))) + .thenReturn(activityProp) + val appProp = PackageManager.Property("", true, "", "") + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER))) + .thenReturn(appProp) + + // Expect activity property to override application property + assertEquals(false, supportsMultiInstance(pm, component, TEST_OTHER_USER)) + } + + @Test + @Throws(RemoteException::class) + fun supportsMultiInstanceSplit_noActivityPropertyApplicationPropertyTrue() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER))) + .thenThrow(RemoteException()) + val appProp = PackageManager.Property("", true, "", "") + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER))) + .thenReturn(appProp) + + // Expect fall through to app property + assertEquals(true, supportsMultiInstance(pm, component, TEST_OTHER_USER)) + } + + @Test + @Throws(RemoteException::class) + fun supportsMultiInstanceSplit_noActivityOrAppProperty() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(component.className), eq(TEST_OTHER_USER))) + .thenThrow(RemoteException()) + whenever(pm.getPropertyAsUser(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName), eq(null), eq(TEST_OTHER_USER))) + .thenThrow(RemoteException()) + + assertEquals(false, supportsMultiInstance(pm, component, TEST_OTHER_USER)) + } + + companion object { + val TEST_PACKAGE = "com.android.server.pm" + val TEST_ACTIVITY = "TestActivity" + val TEST_OTHER_USER = 1234 + } +}
\ No newline at end of file |