Merge "CTA2075: promoting the LoudnessCodecApiTest to presubmit" into main
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index a863870..69273df 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2518,6 +2518,7 @@
USER_MIN_ASPECT_RATIO_16_9,
USER_MIN_ASPECT_RATIO_3_2,
USER_MIN_ASPECT_RATIO_FULLSCREEN,
+ USER_MIN_ASPECT_RATIO_APP_DEFAULT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface UserMinAspectRatio {}
@@ -2571,6 +2572,16 @@
*/
public static final int USER_MIN_ASPECT_RATIO_FULLSCREEN = 6;
+ /**
+ * Aspect ratio override code: user sets to app's default aspect ratio.
+ * This resets both the user-forced aspect ratio, and the device manufacturer
+ * per-app override {@link ActivityInfo#OVERRIDE_ANY_ORIENTATION_TO_USER}.
+ * It is different from {@link #USER_MIN_ASPECT_RATIO_UNSET} as the latter may
+ * apply the device manufacturer per-app orientation override if any,
+ * @hide
+ */
+ public static final int USER_MIN_ASPECT_RATIO_APP_DEFAULT = 7;
+
/** @hide */
@IntDef(flag = true, prefix = { "DELETE_" }, value = {
DELETE_KEEP_DATA,
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 9a1796f..c7797c7 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -64,3 +64,10 @@
bug: "296829976"
is_fixed_read_only: true
}
+
+flag {
+ name: "allow_resolver_sheet_for_private_space"
+ namespace: "profile_experiences"
+ description: "Add support for Private Space in resolver sheet"
+ bug: "307515485"
+}
\ No newline at end of file
diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java
index 40e03db..60ad8e8 100644
--- a/core/java/android/hardware/SystemSensorManager.java
+++ b/core/java/android/hardware/SystemSensorManager.java
@@ -86,6 +86,8 @@
private static native long nativeCreate(String opPackageName);
private static native boolean nativeGetSensorAtIndex(long nativeInstance,
Sensor sensor, int index);
+ private static native boolean nativeGetDefaultDeviceSensorAtIndex(long nativeInstance,
+ Sensor sensor, int index);
private static native void nativeGetDynamicSensors(long nativeInstance, List<Sensor> list);
private static native void nativeGetRuntimeSensors(
long nativeInstance, int deviceId, List<Sensor> list);
@@ -162,11 +164,14 @@
// initialize the sensor list
for (int index = 0;; ++index) {
Sensor sensor = new Sensor();
- if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break;
+ if (android.companion.virtual.flags.Flags.enableNativeVdm()) {
+ if (!nativeGetDefaultDeviceSensorAtIndex(mNativeInstance, sensor, index)) break;
+ } else {
+ if (!nativeGetSensorAtIndex(mNativeInstance, sensor, index)) break;
+ }
mFullSensorsList.add(sensor);
mHandleToSensor.put(sensor.getHandle(), sensor);
}
-
}
/** @hide */
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index ffd7212..64a62a9 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -751,6 +751,23 @@
*/
boolean blockScreenOn(Runnable unblocker);
+ /**
+ * Get the brightness levels used to determine automatic brightness based on lux levels.
+ * @param mode The auto-brightness mode
+ * (AutomaticBrightnessController.AutomaticBrightnessMode)
+ * @return The brightness levels for the specified mode. The values are between
+ * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}.
+ */
+ float[] getAutoBrightnessLevels(int mode);
+
+ /**
+ * Get the lux levels used to determine automatic brightness.
+ * @param mode The auto-brightness mode
+ * (AutomaticBrightnessController.AutomaticBrightnessMode)
+ * @return The lux levels for the specified mode
+ */
+ float[] getAutoBrightnessLuxLevels(int mode);
+
/** Returns whether displayoffload supports the given display state. */
static boolean isSupportedOffloadState(int displayState) {
return Display.isSuspendedState(displayState);
diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
index bd087f9..41dee3a 100644
--- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -21,10 +21,10 @@
package android.nfc.cardemulation;
import android.annotation.FlaggedApi;
-import android.compat.annotation.UnsupportedAppUsage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -374,7 +374,7 @@
// Set uid
mUid = si.applicationInfo.uid;
- mCategoryOtherServiceEnabled = false; // support other category
+ mCategoryOtherServiceEnabled = true; // support other category
}
diff --git a/core/java/android/nfc/flags.aconfig b/core/java/android/nfc/flags.aconfig
index 17e0427..0d073cc 100644
--- a/core/java/android/nfc/flags.aconfig
+++ b/core/java/android/nfc/flags.aconfig
@@ -48,3 +48,10 @@
description: "Enable NFC Polling Loop Notifications ST shim"
bug: "294217286"
}
+
+flag {
+ name: "enable_tag_detection_broadcasts"
+ namespace: "nfc"
+ description: "Enable sending broadcasts to Wallet role holder when a tag enters/leaves the field."
+ bug: "306203494"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 942ce971..54cc5f4 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -15021,6 +15021,16 @@
"foreground_service_starts_logging_enabled";
/**
+ * Describes whether AM's AppProfiler should collect PSS even if RSS is the default. This
+ * can be set by a user in developer settings.
+ * Default: 0
+ * @hide
+ */
+ @Readable
+ public static final String FORCE_ENABLE_PSS_PROFILING =
+ "force_enable_pss_profiling";
+
+ /**
* @hide
* @see com.android.server.appbinding.AppBindingConstants
*/
@@ -19628,6 +19638,15 @@
*/
public static final String WEAR_POWER_ANOMALY_SERVICE_ENABLED =
"wear_power_anomaly_service_enabled";
+
+ /**
+ * A boolean that tracks whether Wrist Detection Auto-Locking is enabled.
+ *
+ * @hide
+ */
+ @Readable
+ public static final String WRIST_DETECTION_AUTO_LOCKING_ENABLED =
+ "wear_wrist_detection_auto_locking_enabled";
}
}
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index b56bef3..30524a1 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -50,3 +50,11 @@
description: "Collect sepolicy hash from sysfs"
bug: "308471499"
}
+
+flag {
+ name: "frp_enforcement"
+ namespace: "android_hw_security"
+ description: "This flag controls whether PDB enforces FRP"
+ bug: "290312729"
+ is_fixed_read_only: true
+}
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 7534d29..7dcbbea 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -249,6 +249,7 @@
private UserHandle mCloneProfileUserHandle;
private UserHandle mTabOwnerUserHandleForLaunch;
+ private UserHandle mPrivateProfileUserHandle;
protected final LatencyTracker mLatencyTracker = getLatencyTracker();
@@ -441,6 +442,7 @@
mPersonalProfileUserHandle = fetchPersonalProfileUserHandle();
mWorkProfileUserHandle = fetchWorkProfileUserProfile();
mCloneProfileUserHandle = fetchCloneProfileUserHandle();
+ mPrivateProfileUserHandle = fetchPrivateProfileUserHandle();
mTabOwnerUserHandleForLaunch = fetchTabOwnerUserHandleForLaunch();
// The last argument of createResolverListAdapter is whether to do special handling
@@ -648,7 +650,8 @@
initialIntents,
rList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle());
+ getPersonalProfileUserHandle());
+
QuietModeManager quietModeManager = createQuietModeManager();
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
@@ -747,6 +750,9 @@
}
protected UserHandle getPersonalProfileUserHandle() {
+ if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){
+ return mPrivateProfileUserHandle;
+ }
return mPersonalProfileUserHandle;
}
protected @Nullable UserHandle getWorkProfileUserHandle() {
@@ -761,6 +767,10 @@
return mTabOwnerUserHandleForLaunch;
}
+ protected UserHandle getPrivateProfileUserHandle() {
+ return mPrivateProfileUserHandle;
+ }
+
protected UserHandle fetchPersonalProfileUserHandle() {
// ActivityManager.getCurrentUser() refers to the current Foreground user. When clone/work
// profile is active, we always make the personal tab from the foreground user.
@@ -795,12 +805,28 @@
return mCloneProfileUserHandle;
}
+ protected @Nullable UserHandle fetchPrivateProfileUserHandle() {
+ mPrivateProfileUserHandle = null;
+ UserManager userManager = getSystemService(UserManager.class);
+ for (final UserInfo userInfo :
+ userManager.getProfiles(mPersonalProfileUserHandle.getIdentifier())) {
+ if (userInfo.isPrivateProfile()) {
+ mPrivateProfileUserHandle = userInfo.getUserHandle();
+ break;
+ }
+ }
+ return mPrivateProfileUserHandle;
+ }
+
private UserHandle fetchTabOwnerUserHandleForLaunch() {
- // If we are in work profile's process, return WorkProfile user as owner, otherwise we
- // always return PersonalProfile user as owner
- return UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())
- ? getWorkProfileUserHandle()
- : getPersonalProfileUserHandle();
+ // If we are in work or private profile's process, return WorkProfile/PrivateProfile user
+ // as owner, otherwise we always return PersonalProfile user as owner
+ if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) {
+ return getWorkProfileUserHandle();
+ } else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+ return getPrivateProfileUserHandle();
+ }
+ return getPersonalProfileUserHandle();
}
private boolean hasWorkProfile() {
@@ -816,7 +842,15 @@
&& (UserHandle.myUserId() == getCloneProfileUserHandle().getIdentifier());
}
+ protected final boolean isLaunchedAsPrivateProfile() {
+ return getPrivateProfileUserHandle() != null
+ && (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier());
+ }
+
protected boolean shouldShowTabs() {
+ if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+ return false;
+ }
return hasWorkProfile() && ENABLE_TABBED_VIEW;
}
@@ -2619,6 +2653,11 @@
return resolveInfo.userHandle;
}
+ private boolean privateSpaceEnabled() {
+ return mIsIntentPicker && android.os.Flags.allowPrivateProfile()
+ && android.multiuser.Flags.allowResolverSheetForPrivateSpace();
+ }
+
/**
* An a11y delegate that expands resolver drawer when gesture navigation reaches a partially
* invisible target in the list.
diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java
index c89cfc4..5705b7e 100644
--- a/core/java/com/android/internal/content/PackageMonitor.java
+++ b/core/java/com/android/internal/content/PackageMonitor.java
@@ -37,6 +37,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
+import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -63,6 +64,8 @@
PackageMonitorCallback mPackageMonitorCallback;
+ private Executor mExecutor;
+
@UnsupportedAppUsage
public PackageMonitor() {
final boolean isCore = UserHandle.isCore(android.os.Process.myUid());
@@ -106,8 +109,8 @@
if (mPackageMonitorCallback == null) {
PackageManager pm = mRegisteredContext.getPackageManager();
if (pm != null) {
- mPackageMonitorCallback = new PackageMonitorCallback(this,
- new HandlerExecutor(mRegisteredHandler));
+ mExecutor = new HandlerExecutor(mRegisteredHandler);
+ mPackageMonitorCallback = new PackageMonitorCallback(this);
int userId = user != null ? user.getIdentifier() : mRegisteredContext.getUserId();
pm.registerPackageMonitorCallback(mPackageMonitorCallback, userId);
}
@@ -131,6 +134,7 @@
}
mPackageMonitorCallback = null;
mRegisteredContext = null;
+ mExecutor = null;
}
public void onBeginPackageChanges() {
@@ -362,6 +366,13 @@
doHandlePackageEvent(intent);
}
+
+ private void postHandlePackageEvent(Intent intent) {
+ if (mExecutor != null) {
+ mExecutor.execute(() -> doHandlePackageEvent(intent));
+ }
+ }
+
/**
* Handle the package related event
* @param intent the intent that contains package related event information
@@ -516,13 +527,10 @@
}
private static final class PackageMonitorCallback extends IRemoteCallback.Stub {
+ private final WeakReference<PackageMonitor> mMonitorWeakReference;
- private final PackageMonitor mPackageMonitor;
- private final Executor mExecutor;
-
- PackageMonitorCallback(PackageMonitor monitor, Executor executor) {
- mPackageMonitor = monitor;
- mExecutor = executor;
+ PackageMonitorCallback(PackageMonitor monitor) {
+ mMonitorWeakReference = new WeakReference<>(monitor);
}
@Override
@@ -537,7 +545,10 @@
Log.w(TAG, "No intent is set for PackageMonitorCallback");
return;
}
- mExecutor.execute(() -> mPackageMonitor.doHandlePackageEvent(intent));
+ PackageMonitor monitor = mMonitorWeakReference.get();
+ if (monitor != null) {
+ monitor.postHandlePackageEvent(intent);
+ }
}
}
}
diff --git a/core/jni/android_hardware_SensorManager.cpp b/core/jni/android_hardware_SensorManager.cpp
index 9c883d1..56ea52d 100644
--- a/core/jni/android_hardware_SensorManager.cpp
+++ b/core/jni/android_hardware_SensorManager.cpp
@@ -225,6 +225,19 @@
return translateNativeSensorToJavaSensor(env, sensor, *sensorList[index]) != NULL;
}
+static jboolean nativeGetDefaultDeviceSensorAtIndex(JNIEnv *env, jclass clazz, jlong sensorManager,
+ jobject sensor, jint index) {
+ SensorManager *mgr = reinterpret_cast<SensorManager *>(sensorManager);
+
+ Vector<Sensor> sensorList;
+ ssize_t count = mgr->getDefaultDeviceSensorList(sensorList);
+ if (ssize_t(index) >= count) {
+ return false;
+ }
+
+ return translateNativeSensorToJavaSensor(env, sensor, sensorList[index]) != NULL;
+}
+
static void
nativeGetDynamicSensors(JNIEnv *env, jclass clazz, jlong sensorManager, jobject sensorList) {
@@ -539,6 +552,9 @@
{"nativeGetSensorAtIndex", "(JLandroid/hardware/Sensor;I)Z",
(void *)nativeGetSensorAtIndex},
+ {"nativeGetDefaultDeviceSensorAtIndex", "(JLandroid/hardware/Sensor;I)Z",
+ (void *)nativeGetDefaultDeviceSensorAtIndex},
+
{"nativeGetDynamicSensors", "(JLjava/util/List;)V", (void *)nativeGetDynamicSensors},
{"nativeGetRuntimeSensors", "(JILjava/util/List;)V", (void *)nativeGetRuntimeSensors},
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index b6813ff..b209c7c 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -30,6 +30,7 @@
import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo;
import static com.android.internal.app.ResolverWrapperActivity.sOverrides;
+import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.allOf;
@@ -46,6 +47,7 @@
import android.net.Uri;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.text.TextUtils;
import android.view.View;
import android.widget.RelativeLayout;
@@ -88,7 +90,8 @@
public ActivityTestRule<ResolverWrapperActivity> mActivityRule =
new ActivityTestRule<>(ResolverWrapperActivity.class, false,
false);
-
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void cleanOverrideData() {
sOverrides.reset();
@@ -1156,6 +1159,97 @@
sOverrides.cloneProfileUserHandle)));
}
+ @Test
+ public void testTriggerFromPrivateProfile_withoutWorkProfile() throws RemoteException {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markPrivateProfileUserAvailable();
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> privateResolvedComponentInfos =
+ createResolvedComponentsForTest(3, sOverrides.privateProfileUserHandle);
+ setupResolverControllers(privateResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+ onView(withId(R.id.button_once)).check(matches(not(isEnabled())));
+ onView(withId(R.id.button_always)).check(matches(not(isEnabled())));
+ for (ResolvedComponentInfo resolvedInfo : privateResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+ sOverrides.privateProfileUserHandle);
+ }
+ }
+
+ @Test
+ public void testTriggerFromPrivateProfile_withWorkProfilePresent(){
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ ResolverActivity.ENABLE_TABBED_VIEW = false;
+ markPrivateProfileUserAvailable();
+ markWorkProfileUserAvailable();
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> privateResolvedComponentInfos =
+ createResolvedComponentsForTest(3, sOverrides.privateProfileUserHandle);
+ setupResolverControllers(privateResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+ for (ResolvedComponentInfo resolvedInfo : privateResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+ sOverrides.privateProfileUserHandle);
+ }
+ }
+
+ @Test
+ public void testPrivateProfile_triggerFromPrimaryUser_withWorkProfilePresent(){
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markPrivateProfileUserAvailable();
+ markWorkProfileUserAvailable();
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ onView(withId(R.id.tabs)).check(matches(isDisplayed()));
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+ activity.getPersonalProfileUserHandle());
+ }
+ }
+
+ @Test
+ public void testPrivateProfile_triggerFromWorkProfile(){
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markPrivateProfileUserAvailable();
+ markWorkProfileUserAvailable();
+ Intent sendIntent = createSendImageIntent();
+
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ onView(withId(R.id.tabs)).check(matches(isDisplayed()));
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertTrue(resolvedInfo.getResolveInfoAt(0).userHandle.equals(
+ activity.getPersonalProfileUserHandle()) || resolvedInfo.getResolveInfoAt(
+ 0).userHandle.equals(activity.getWorkProfileUserHandle()));
+ }
+ }
+
private Intent createSendImageIntent() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
@@ -1237,6 +1331,10 @@
ResolverWrapperActivity.sOverrides.cloneProfileUserHandle = UserHandle.of(11);
}
+ private void markPrivateProfileUserAvailable() {
+ ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12);
+ }
+
private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
@@ -1256,4 +1354,13 @@
eq(UserHandle.SYSTEM)))
.thenReturn(new ArrayList<>(personalResolvedComponentInfos));
}
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> resolvedComponentInfos) {
+ when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(resolvedComponentInfos));
+ }
}
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index e193de0..862cbd5 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -88,6 +88,10 @@
return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1));
}
+ int getMultiProfilePagerAdapterCount(){
+ return mMultiProfilePagerAdapter.getCount();
+ }
+
@Override
public boolean isVoiceInteraction() {
if (sOverrides.isVoiceInteraction != null) {
@@ -144,6 +148,11 @@
}
@Override
+ protected UserHandle getPrivateProfileUserHandle() {
+ return sOverrides.privateProfileUserHandle;
+ }
+
+ @Override
protected UserHandle getTabOwnerUserHandleForLaunch() {
if (sOverrides.tabOwnerUserHandleForLaunch == null) {
return super.getTabOwnerUserHandleForLaunch();
@@ -176,6 +185,7 @@
public Boolean isVoiceInteraction;
public UserHandle workProfileUserHandle;
public UserHandle cloneProfileUserHandle;
+ public UserHandle privateProfileUserHandle;
public UserHandle tabOwnerUserHandleForLaunch;
public Integer myUserId;
public boolean hasCrossProfileIntents;
@@ -191,6 +201,7 @@
workResolverListController = mock(ResolverListController.class);
workProfileUserHandle = null;
cloneProfileUserHandle = null;
+ privateProfileUserHandle = null;
tabOwnerUserHandleForLaunch = null;
myUserId = null;
hasCrossProfileIntents = true;
diff --git a/core/tests/overlaytests/Android.mk b/core/tests/overlaytests/Android.mk
deleted file mode 100644
index b798d87..0000000
--- a/core/tests/overlaytests/Android.mk
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (C) 2017 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.
-
-include $(call all-subdir-makefiles)
diff --git a/core/tests/overlaytests/host/Android.mk b/core/tests/overlaytests/host/Android.mk
deleted file mode 100644
index d58d939..0000000
--- a/core/tests/overlaytests/host/Android.mk
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright (C) 2018 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.
-
-LOCAL_PATH := $(call my-dir)
-
-# Include to build test-apps.
-include $(call all-makefiles-under,$(LOCAL_PATH))
-
diff --git a/core/tests/overlaytests/host/test-apps/Android.mk b/core/tests/overlaytests/host/test-apps/Android.mk
deleted file mode 100644
index 5c7187e..0000000
--- a/core/tests/overlaytests/host/test-apps/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-# Copyright (C) 2018 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.
-
-include $(call all-subdir-makefiles)
-
diff --git a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.bp b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.bp
new file mode 100644
index 0000000..bb7d63e
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.bp
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 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.
+
+// Error: Cannot get the name of the license module in the
+// ./Android.bp file.
+// If no such license module exists, please add one there first.
+// Then reset the default_applicable_licenses property below with the license module name.
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_NonPlatformSignatureOverlay",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ aaptflags: [
+ "--custom-package com.android.server.om.hosttest.signature_overlay_bad",
+ ],
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_PlatformSignatureStaticOverlay",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ certificate: "platform",
+ manifest: "static/AndroidManifest.xml",
+ aaptflags: [
+ "--custom-package com.android.server.om.hosttest.signature_overlay_static",
+ ],
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_PlatformSignatureOverlay",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ certificate: "platform",
+ aaptflags: [
+ "--custom-package",
+ "com.android.server.om.hosttest.signature_overlay_v1",
+ "--version-code",
+ "1",
+ "--version-name",
+ "v1",
+ ],
+}
diff --git a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
deleted file mode 100644
index b453cde9..0000000
--- a/core/tests/overlaytests/host/test-apps/SignatureOverlay/Android.mk
+++ /dev/null
@@ -1,56 +0,0 @@
-# Copyright (C) 2018 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.
-
-LOCAL_PATH := $(call my-dir)
-
-my_package_prefix := com.android.server.om.hosttest.signature_overlay
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_NonPlatformSignatureOverlay
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_bad
-include $(BUILD_PACKAGE)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_PlatformSignatureStaticOverlay
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
-LOCAL_MANIFEST_FILE := static/AndroidManifest.xml
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_static
-include $(BUILD_PACKAGE)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_PlatformSignatureOverlay
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
-LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
-include $(BUILD_PACKAGE)
-
-my_package_prefix :=
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.bp b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.bp
new file mode 100644
index 0000000..ee0c0e5
--- /dev/null
+++ b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.bp
@@ -0,0 +1,97 @@
+// Copyright (C) 2018 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.
+
+// Error: Cannot get the name of the license module in the
+// ./Android.bp file.
+// If no such license module exists, please add one there first.
+// Then reset the default_applicable_licenses property below with the license module name.
+package {
+ // See: http://go/android-license-faq
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_UpdateOverlay",
+ srcs: ["src/**/*.java"],
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ static_libs: ["androidx.test.rules"],
+ aaptflags: ["--no-resource-removal"],
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_FrameworkOverlayV1",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ certificate: "platform",
+ aaptflags: [
+ "--custom-package",
+ "com.android.server.om.hosttest.framework_overlay_v1",
+ "--version-code",
+ "1",
+ "--version-name",
+ "v1",
+ ],
+ resource_dirs: ["framework/v1/res"],
+ manifest: "framework/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_FrameworkOverlayV2",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ certificate: "platform",
+ aaptflags: [
+ "--custom-package",
+ "com.android.server.om.hosttest.framework_overlay_v2",
+ "--version-code",
+ "2",
+ "--version-name",
+ "v2",
+ ],
+ resource_dirs: ["framework/v2/res"],
+ manifest: "framework/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_AppOverlayV1",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ aaptflags: [
+ "--custom-package",
+ "com.android.server.om.hosttest.app_overlay_v1",
+ "--version-code",
+ "1",
+ "--version-name",
+ "v1",
+ ],
+ resource_dirs: ["app/v1/res"],
+ manifest: "app/v1/AndroidManifest.xml",
+}
+
+android_test_helper_app {
+ name: "OverlayHostTests_AppOverlayV2",
+ sdk_version: "current",
+ test_suites: ["device-tests"],
+ aaptflags: [
+ "--custom-package",
+ "com.android.server.om.hosttest.app_overlay_v2",
+ "--version-code",
+ "2",
+ "--version-name",
+ "v2",
+ ],
+ resource_dirs: ["app/v2/res"],
+ manifest: "app/v2/AndroidManifest.xml",
+}
diff --git a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk b/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
deleted file mode 100644
index 77fc887..0000000
--- a/core/tests/overlaytests/host/test-apps/UpdateOverlay/Android.mk
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright (C) 2018 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.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_SRC_FILES := $(call all-java-files-under,src)
-LOCAL_PACKAGE_NAME := OverlayHostTests_UpdateOverlay
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
-LOCAL_USE_AAPT2 := true
-LOCAL_AAPT_FLAGS := --no-resource-removal
-include $(BUILD_PACKAGE)
-
-my_package_prefix := com.android.server.om.hosttest.framework_overlay
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_FrameworkOverlayV1
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
-LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/framework/v1/res
-LOCAL_MANIFEST_FILE := framework/AndroidManifest.xml
-include $(BUILD_PACKAGE)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_FrameworkOverlayV2
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
-LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/framework/v2/res
-LOCAL_MANIFEST_FILE := framework/AndroidManifest.xml
-include $(BUILD_PACKAGE)
-
-my_package_prefix := com.android.server.om.hosttest.app_overlay
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_AppOverlayV1
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v1
-LOCAL_AAPT_FLAGS += --version-code 1 --version-name v1
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v1/res
-LOCAL_MANIFEST_FILE := app/v1/AndroidManifest.xml
-include $(BUILD_PACKAGE)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE_TAGS := tests
-LOCAL_PACKAGE_NAME := OverlayHostTests_AppOverlayV2
-LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
-LOCAL_LICENSE_CONDITIONS := notice
-LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../../NOTICE
-LOCAL_SDK_VERSION := current
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_AAPT_FLAGS := --custom-package $(my_package_prefix)_v2
-LOCAL_AAPT_FLAGS += --version-code 2 --version-name v2
-LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/app/v2/res
-LOCAL_MANIFEST_FILE := app/v2/AndroidManifest.xml
-include $(BUILD_PACKAGE)
-
-my_package_prefix :=
diff --git a/data/etc/platform.xml b/data/etc/platform.xml
index c4530f6..13d38d2 100644
--- a/data/etc/platform.xml
+++ b/data/etc/platform.xml
@@ -347,7 +347,9 @@
<!-- Allow IMS service entitlement app to schedule jobs to run when app in background. -->
<allow-in-power-save-except-idle package="com.android.imsserviceentitlement" />
- <!-- Allow device lock controller app to schedule jobs and alarms when app in background,
- otherwise, it may not be able to enforce provision for managed devices. -->
+ <!-- Allow device lock controller app to schedule jobs and alarms, and have network access
+ when app in background; otherwise, it may not be able to enforce provision for managed
+ devices. -->
<allow-in-power-save package="com.android.devicelockcontroller" />
+ <allow-in-data-usage-save package="com.android.devicelockcontroller" />
</permissions>
diff --git a/packages/CompanionDeviceManager/res/layout/data_transfer_confirmation.xml b/packages/CompanionDeviceManager/res/layout/data_transfer_confirmation.xml
index db8ebb4..1ac5db6 100644
--- a/packages/CompanionDeviceManager/res/layout/data_transfer_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/data_transfer_confirmation.xml
@@ -18,54 +18,63 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- style="@style/ScrollViewStyle">
+ style="@style/ScrollViewStyle"
+ android:importantForAccessibility="no">
<LinearLayout
- android:id="@+id/data_transfer_confirmation"
- style="@style/ContainerLayout">
-
- <!-- Do NOT change the ID of the root LinearLayout above: it's referenced in CTS tests. -->
-
- <ImageView
- android:id="@+id/header_icon"
- android:layout_width="match_parent"
- android:layout_height="32dp"
- android:gravity="center"
- android:layout_marginTop="18dp"
- android:src="@drawable/ic_warning"
- android:contentDescription="@null" />
-
- <LinearLayout style="@style/Description">
-
- <TextView
- android:id="@+id/title"
- style="@style/DescriptionTitle" />
-
- <TextView
- android:id="@+id/summary"
- style="@style/DescriptionSummary" />
-
- </LinearLayout>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:importantForAccessibility="no">
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="vertical"
- android:layout_marginTop="12dp"
- android:layout_marginBottom="18dp">
+ android:id="@+id/data_transfer_confirmation"
+ style="@style/ContainerLayout">
- <!-- Do NOT change the IDs of the buttons: they are referenced in CTS tests. -->
+ <!-- Do NOT change the ID of the root LinearLayout above: it's referenced in CTS tests. -->
- <Button
- android:id="@+id/btn_positive"
- style="@style/PositiveButton"
- android:text="@string/consent_yes" />
+ <ImageView
+ android:id="@+id/header_icon"
+ android:layout_width="match_parent"
+ android:layout_height="32dp"
+ android:gravity="center"
+ android:layout_marginTop="18dp"
+ android:src="@drawable/ic_warning"
+ android:contentDescription="@null" />
- <Button
- android:id="@+id/btn_negative"
- style="@style/NegativeButton"
- android:text="@string/consent_no" />
+ <LinearLayout style="@style/Description">
+
+ <TextView
+ android:id="@+id/title"
+ style="@style/DescriptionTitle" />
+
+ <TextView
+ android:id="@+id/summary"
+ style="@style/DescriptionSummary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="18dp">
+
+ <!-- Do NOT change the IDs of the buttons: they are referenced in CTS tests. -->
+
+ <Button
+ android:id="@+id/btn_positive"
+ style="@style/PositiveButton"
+ android:text="@string/consent_yes" />
+
+ <Button
+ android:id="@+id/btn_negative"
+ style="@style/NegativeButton"
+ android:text="@string/consent_no" />
+
+ </LinearLayout>
</LinearLayout>
diff --git a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
index 933be11..6a4bb21 100644
--- a/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
+++ b/packages/LocalTransport/src/com/android/localtransport/LocalTransport.java
@@ -134,8 +134,7 @@
@UsesReflection({
// As the runtime class name is used to generate the returned name, and the returned
// name may be used used with reflection, generate the necessary keep rules.
- @KeepTarget(classConstant = LocalTransport.class),
- @KeepTarget(extendsClassConstant = LocalTransport.class)
+ @KeepTarget(instanceOfClassConstant = LocalTransport.class)
})
@Override
public String name() {
diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp
index 25ad9b8..98a5a67 100644
--- a/packages/PackageInstaller/Android.bp
+++ b/packages/PackageInstaller/Android.bp
@@ -35,7 +35,10 @@
name: "PackageInstaller",
defaults: ["platform_app_defaults"],
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
certificate: "platform",
privileged: true,
@@ -62,7 +65,10 @@
name: "PackageInstaller_tablet",
defaults: ["platform_app_defaults"],
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
certificate: "platform",
privileged: true,
@@ -91,7 +97,10 @@
name: "PackageInstaller_tv",
defaults: ["platform_app_defaults"],
- srcs: ["src/**/*.java"],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
certificate: "platform",
privileged: true,
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
deleted file mode 100644
index c8175ad..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
+++ /dev/null
@@ -1,912 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model;
-
-import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery;
-import static com.android.packageinstaller.v2.model.PackageUtil.generateStubPackageInfo;
-import static com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet;
-import static com.android.packageinstaller.v2.model.PackageUtil.getPackageInfo;
-import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid;
-import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
-import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
-import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_DONE;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.DLG_PACKAGE_ERROR;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION;
-import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.AppOpsManager;
-import android.app.PendingIntent;
-import android.app.admin.DevicePolicyManager;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.InstallSourceInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.ApplicationInfoFlags;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.AssetFileDescriptor;
-import android.net.Uri;
-import android.os.ParcelFileDescriptor;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.text.TextUtils;
-import android.util.EventLog;
-import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.MutableLiveData;
-import com.android.packageinstaller.R;
-import com.android.packageinstaller.common.EventResultPersister;
-import com.android.packageinstaller.common.InstallEventReceiver;
-import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
-import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
-import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
-import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
-import com.android.packageinstaller.v2.model.installstagedata.InstallReady;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
-import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
-import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
-import java.io.File;
-import java.io.IOException;
-
-public class InstallRepository {
-
- public static final String EXTRA_STAGED_SESSION_ID =
- "com.android.packageinstaller.extra.STAGED_SESSION_ID";
- private static final String SCHEME_PACKAGE = "package";
- private static final String BROADCAST_ACTION =
- "com.android.packageinstaller.ACTION_INSTALL_COMMIT";
- private static final String TAG = InstallRepository.class.getSimpleName();
- private final Context mContext;
- private final PackageManager mPackageManager;
- private final PackageInstaller mPackageInstaller;
- private final UserManager mUserManager;
- private final DevicePolicyManager mDevicePolicyManager;
- private final AppOpsManager mAppOpsManager;
- private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>();
- private final MutableLiveData<InstallStage> mInstallResult = new MutableLiveData<>();
- private final boolean mLocalLOGV = false;
- private Intent mIntent;
- private boolean mIsSessionInstall;
- private boolean mIsTrustedSource;
- /**
- * Session ID for a session created when caller uses PackageInstaller APIs
- */
- private int mSessionId;
- /**
- * Session ID for a session created by this app
- */
- private int mStagedSessionId = SessionInfo.INVALID_ID;
- private int mCallingUid;
- private String mCallingPackage;
- private SessionStager mSessionStager;
- private AppOpRequestInfo mAppOpRequestInfo;
- private AppSnippet mAppSnippet;
- /**
- * PackageInfo of the app being installed on device.
- */
- private PackageInfo mNewPackageInfo;
-
- public InstallRepository(Context context) {
- mContext = context;
- mPackageManager = context.getPackageManager();
- mPackageInstaller = mPackageManager.getPackageInstaller();
- mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
- mUserManager = context.getSystemService(UserManager.class);
- mAppOpsManager = context.getSystemService(AppOpsManager.class);
- }
-
- /**
- * Extracts information from the incoming install intent, checks caller's permission to install
- * packages, verifies that the caller is the install session owner (in case of a session based
- * install) and checks if the current user has restrictions set that prevent app installation,
- *
- * @param intent the incoming {@link Intent} object for installing a package
- * @param callerInfo {@link CallerInfo} that holds the callingUid and callingPackageName
- * @return <p>{@link InstallAborted} if there are errors while performing the checks</p>
- * <p>{@link InstallStaging} after successfully performing the checks</p>
- */
- public InstallStage performPreInstallChecks(Intent intent, CallerInfo callerInfo) {
- mIntent = intent;
-
- String callingAttributionTag = null;
-
- mIsSessionInstall =
- PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
- || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
-
- mSessionId = mIsSessionInstall
- ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
- : SessionInfo.INVALID_ID;
-
- mStagedSessionId = mIntent.getIntExtra(EXTRA_STAGED_SESSION_ID, SessionInfo.INVALID_ID);
-
- mCallingPackage = callerInfo.getPackageName();
-
- if (mCallingPackage == null && mSessionId != SessionInfo.INVALID_ID) {
- PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(mSessionId);
- mCallingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null;
- callingAttributionTag =
- (sessionInfo != null) ? sessionInfo.getInstallerAttributionTag() : null;
- }
-
- // Uid of the source package, coming from ActivityManager
- mCallingUid = callerInfo.getUid();
- if (mCallingUid == Process.INVALID_UID) {
- Log.e(TAG, "Could not determine the launching uid.");
- }
- final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage);
- // Uid of the source package, with a preference to uid from ApplicationInfo
- final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid;
- mAppOpRequestInfo = new AppOpRequestInfo(
- getPackageNameForUid(mContext, originatingUid, mCallingPackage),
- originatingUid, callingAttributionTag);
-
- if (mCallingUid == Process.INVALID_UID && sourceInfo == null) {
- // Caller's identity could not be determined. Abort the install
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
-
- if ((mSessionId != SessionInfo.INVALID_ID
- && !isCallerSessionOwner(mPackageInstaller, originatingUid, mSessionId))
- || (mStagedSessionId != SessionInfo.INVALID_ID
- && !isCallerSessionOwner(mPackageInstaller, Process.myUid(), mStagedSessionId))) {
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
-
- mIsTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, mIntent, originatingUid);
-
- if (!isInstallPermissionGrantedOrRequested(mContext, mCallingUid, originatingUid,
- mIsTrustedSource)) {
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
-
- String restriction = getDevicePolicyRestrictions();
- if (restriction != null) {
- InstallAborted.Builder abortedBuilder =
- new InstallAborted.Builder(ABORT_REASON_POLICY).setMessage(restriction);
- final Intent adminSupportDetailsIntent =
- mDevicePolicyManager.createAdminSupportIntent(restriction);
- if (adminSupportDetailsIntent != null) {
- abortedBuilder.setResultIntent(adminSupportDetailsIntent);
- }
- return abortedBuilder.build();
- }
-
- maybeRemoveInvalidInstallerPackageName(callerInfo);
-
- return new InstallStaging();
- }
-
- /**
- * @return the ApplicationInfo for the installation source (the calling package), if available
- */
- @Nullable
- private ApplicationInfo getSourceInfo(@Nullable String callingPackage) {
- if (callingPackage == null) {
- return null;
- }
- try {
- return mPackageManager.getApplicationInfo(callingPackage, 0);
- } catch (PackageManager.NameNotFoundException ignored) {
- return null;
- }
- }
-
- private boolean isInstallRequestFromTrustedSource(ApplicationInfo sourceInfo, Intent intent,
- int originatingUid) {
- boolean isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
- return sourceInfo != null && sourceInfo.isPrivilegedApp()
- && (isNotUnknownSource
- || isPermissionGranted(mContext, Manifest.permission.INSTALL_PACKAGES, originatingUid));
- }
-
- private String getDevicePolicyRestrictions() {
- final String[] restrictions = new String[]{
- UserManager.DISALLOW_INSTALL_APPS,
- UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
- UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
- };
-
- for (String restriction : restrictions) {
- if (!mUserManager.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
- continue;
- }
- return restriction;
- }
- return null;
- }
-
- private void maybeRemoveInvalidInstallerPackageName(CallerInfo callerInfo) {
- final String installerPackageNameFromIntent =
- mIntent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
- if (installerPackageNameFromIntent == null) {
- return;
- }
- if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.getPackageName())
- && !isPermissionGranted(mPackageManager, Manifest.permission.INSTALL_PACKAGES,
- callerInfo.getPackageName())) {
- Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent
- + " is invalid. Remove it.");
- EventLog.writeEvent(0x534e4554, "236687884", callerInfo.getUid(),
- "Invalid EXTRA_INSTALLER_PACKAGE_NAME");
- mIntent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);
- }
- }
-
- public void stageForInstall() {
- Uri uri = mIntent.getData();
- if (mStagedSessionId != SessionInfo.INVALID_ID
- || mIsSessionInstall
- || (uri != null && SCHEME_PACKAGE.equals(uri.getScheme()))) {
- // For a session based install or installing with a package:// URI, there is no file
- // for us to stage.
- mStagingResult.setValue(new InstallReady());
- return;
- }
- if (uri != null
- && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
- && canPackageQuery(mContext, mCallingUid, uri)) {
-
- if (mStagedSessionId > 0) {
- final PackageInstaller.SessionInfo info =
- mPackageInstaller.getSessionInfo(mStagedSessionId);
- if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) {
- Log.w(TAG, "Session " + mStagedSessionId + " in funky state; ignoring");
- if (info != null) {
- cleanupStagingSession();
- }
- mStagedSessionId = 0;
- }
- }
-
- // Session does not exist, or became invalid.
- if (mStagedSessionId <= 0) {
- // Create session here to be able to show error.
- try (final AssetFileDescriptor afd =
- mContext.getContentResolver().openAssetFileDescriptor(uri, "r")) {
- ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
- PackageInstaller.SessionParams params =
- createSessionParams(mIntent, pfd, uri.toString());
- mStagedSessionId = mPackageInstaller.createSession(params);
- } catch (IOException e) {
- Log.w(TAG, "Failed to create a staging session", e);
- mStagingResult.setValue(
- new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_APK))
- .setActivityResultCode(Activity.RESULT_FIRST_USER)
- .build());
- return;
- }
- }
-
- SessionStageListener listener = new SessionStageListener() {
- @Override
- public void onStagingSuccess(SessionInfo info) {
- //TODO: Verify if the returned sessionInfo should be used anywhere
- mStagingResult.setValue(new InstallReady());
- }
-
- @Override
- public void onStagingFailure() {
- cleanupStagingSession();
- mStagingResult.setValue(
- new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_APK))
- .setActivityResultCode(Activity.RESULT_FIRST_USER)
- .build());
- }
- };
- if (mSessionStager != null) {
- mSessionStager.cancel(true);
- }
- mSessionStager = new SessionStager(mContext, uri, mStagedSessionId, listener);
- mSessionStager.execute();
- }
- }
-
- public int getStagedSessionId() {
- return mStagedSessionId;
- }
-
- private void cleanupStagingSession() {
- if (mStagedSessionId > 0) {
- try {
- mPackageInstaller.abandonSession(mStagedSessionId);
- } catch (SecurityException ignored) {
- }
- mStagedSessionId = 0;
- }
- }
-
- private PackageInstaller.SessionParams createSessionParams(@NonNull Intent intent,
- @Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) {
- PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
- PackageInstaller.SessionParams.MODE_FULL_INSTALL);
- final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri.class);
- params.setPackageSource(
- referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
- : PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
- params.setInstallAsInstantApp(false);
- params.setReferrerUri(referrerUri);
- params.setOriginatingUri(
- intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri.class));
- params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
- Process.INVALID_UID));
- params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME));
- params.setInstallReason(PackageManager.INSTALL_REASON_USER);
- // Disable full screen intent usage by for sideloads.
- params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
- PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
-
- if (pfd != null) {
- try {
- final PackageInstaller.InstallInfo result = mPackageInstaller.readInstallInfo(pfd,
- debugPathName, 0);
- params.setAppPackageName(result.getPackageName());
- params.setInstallLocation(result.getInstallLocation());
- params.setSize(result.calculateInstalledSize(params, pfd));
- } catch (PackageInstaller.PackageParsingException e) {
- Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e);
- params.setSize(pfd.getStatSize());
- } catch (IOException e) {
- Log.e(TAG,
- "Cannot calculate installed size " + debugPathName
- + ". Try only apk size.", e);
- }
- } else {
- Log.e(TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.");
- }
- return params;
- }
-
- /**
- * Processes Install session, file:// or package:// URI to generate data pertaining to user
- * confirmation for an install. This method also checks if the source app has the AppOp granted
- * to install unknown apps. If an AppOp is to be requested, cache the user action prompt data to
- * be reused once appOp has been granted
- *
- * @return <ul>
- * <li>InstallAborted </li>
- * <ul>
- * <li> If install session is invalid (not sealed or resolvedBaseApk path
- * is invalid) </li>
- * <li> Source app doesn't have visibility to target app </li>
- * <li> The APK is invalid </li>
- * <li> URI is invalid </li>
- * <li> Can't get ApplicationInfo for source app, to request AppOp </li>
- * </ul>
- * <li> InstallUserActionRequired</li>
- * <ul>
- * <li> If AppOP is granted and user action is required to proceed
- * with install </li>
- * <li> If AppOp grant is to be requested from the user</li>
- * </ul>
- * </ul>
- */
- public InstallStage requestUserConfirmation() {
- if (mIsTrustedSource) {
- if (mLocalLOGV) {
- Log.i(TAG, "install allowed");
- }
- // Returns InstallUserActionRequired stage if install details could be successfully
- // computed, else it returns InstallAborted.
- return generateConfirmationSnippet();
- } else {
- InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo);
- if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) {
- // Source app already has appOp granted.
- return generateConfirmationSnippet();
- } else {
- return unknownSourceStage;
- }
- }
- }
-
-
- private InstallStage generateConfirmationSnippet() {
- final Object packageSource;
- int pendingUserActionReason = -1;
- if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(mIntent.getAction())) {
- final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId);
- String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null;
-
- if (info == null || !info.isSealed() || resolvedPath == null) {
- Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- packageSource = Uri.fromFile(new File(resolvedPath));
- // TODO: Not sure where is this used yet. PIA.java passes it to
- // InstallInstalling if not null
- // mOriginatingURI = null;
- // mReferrerURI = null;
- pendingUserActionReason = info.getPendingUserActionReason();
- } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(mIntent.getAction())) {
- final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId);
-
- if (info == null || !info.isPreApprovalRequested()) {
- Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- packageSource = info;
- // mOriginatingURI = null;
- // mReferrerURI = null;
- pendingUserActionReason = info.getPendingUserActionReason();
- } else {
- // Two possible origins:
- // 1. Installation with SCHEME_PACKAGE.
- // 2. Installation with "file://" for session created by this app
- if (mIntent.getData() != null && mIntent.getData().getScheme().equals(SCHEME_PACKAGE)) {
- packageSource = mIntent.getData();
- } else {
- SessionInfo stagedSessionInfo = mPackageInstaller.getSessionInfo(mStagedSessionId);
- packageSource = Uri.fromFile(new File(stagedSessionInfo.getResolvedBaseApkPath()));
- }
- // mOriginatingURI = mIntent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
- // mReferrerURI = mIntent.getParcelableExtra(Intent.EXTRA_REFERRER);
- pendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE;
- }
-
- // if there's nothing to do, quietly slip into the ether
- if (packageSource == null) {
- Log.w(TAG, "Unspecified source");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_URI))
- .setActivityResultCode(Activity.RESULT_FIRST_USER)
- .build();
- }
-
- return processAppSnippet(packageSource, pendingUserActionReason);
- }
-
- /**
- * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install
- * session) to set up the installer for this install.
- *
- * @param source The source of package URI or SessionInfo
- * @return {@code true} iff the installer could be set up
- */
- private InstallStage processAppSnippet(Object source, int userActionReason) {
- if (source instanceof Uri) {
- return processPackageUri((Uri) source, userActionReason);
- } else if (source instanceof SessionInfo) {
- return processSessionInfo((SessionInfo) source, userActionReason);
- }
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
-
- /**
- * Parse the Uri and set up the installer for this package.
- *
- * @param packageUri The URI to parse
- * @return {@code true} iff the installer could be set up
- */
- private InstallStage processPackageUri(final Uri packageUri, int userActionReason) {
- final String scheme = packageUri.getScheme();
- final String packageName = packageUri.getSchemeSpecificPart();
-
- if (scheme == null) {
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
-
- if (mLocalLOGV) {
- Log.i(TAG, "processPackageUri(): uri = " + packageUri + ", scheme = " + scheme);
- }
-
- switch (scheme) {
- case SCHEME_PACKAGE -> {
- for (UserHandle handle : mUserManager.getUserHandles(true)) {
- PackageManager pmForUser = mContext.createContextAsUser(handle, 0)
- .getPackageManager();
- try {
- if (pmForUser.canPackageQuery(mCallingPackage, packageName)) {
- mNewPackageInfo = pmForUser.getPackageInfo(packageName,
- PackageManager.GET_PERMISSIONS
- | PackageManager.MATCH_UNINSTALLED_PACKAGES);
- }
- } catch (NameNotFoundException ignored) {
- }
- }
- if (mNewPackageInfo == null) {
- Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart()
- + " not available. Discontinuing installation");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .setErrorDialogType(DLG_PACKAGE_ERROR)
- .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_APK))
- .setActivityResultCode(Activity.RESULT_FIRST_USER)
- .build();
- }
- mAppSnippet = getAppSnippet(mContext, mNewPackageInfo);
- if (mLocalLOGV) {
- Log.i(TAG, "Created snippet for " + mAppSnippet.getLabel());
- }
- }
- case ContentResolver.SCHEME_FILE -> {
- File sourceFile = new File(packageUri.getPath());
- mNewPackageInfo = getPackageInfo(mContext, sourceFile,
- PackageManager.GET_PERMISSIONS);
-
- // Check for parse errors
- if (mNewPackageInfo == null) {
- Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .setErrorDialogType(DLG_PACKAGE_ERROR)
- .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
- PackageManager.INSTALL_FAILED_INVALID_APK))
- .setActivityResultCode(Activity.RESULT_FIRST_USER)
- .build();
- }
- if (mLocalLOGV) {
- Log.i(TAG, "Creating snippet for local file " + sourceFile);
- }
- mAppSnippet = getAppSnippet(mContext, mNewPackageInfo.applicationInfo, sourceFile);
- }
- default -> {
- Log.e(TAG, "Unexpected URI scheme " + packageUri);
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- }
-
- return new InstallUserActionRequired.Builder(
- USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet)
- .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason))
- .setAppUpdating(isAppUpdating(mNewPackageInfo))
- .build();
- }
-
- /**
- * Use the SessionInfo and set up the installer for pre-commit install session.
- *
- * @param sessionInfo The SessionInfo to compose
- * @return {@code true} iff the installer could be set up
- */
- private InstallStage processSessionInfo(@NonNull SessionInfo sessionInfo,
- int userActionReason) {
- mNewPackageInfo = generateStubPackageInfo(sessionInfo.getAppPackageName());
-
- mAppSnippet = getAppSnippet(mContext, sessionInfo);
- return new InstallUserActionRequired.Builder(
- USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet)
- .setAppUpdating(isAppUpdating(mNewPackageInfo))
- .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason))
- .build();
- }
-
- private String getUpdateMessage(PackageInfo pkgInfo, int userActionReason) {
- if (isAppUpdating(pkgInfo)) {
- final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo);
- final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage);
-
- if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
- && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) {
- return mContext.getString(R.string.install_confirm_question_update_owner_reminder,
- requestedUpdateOwnerLabel, existingUpdateOwnerLabel);
- }
- }
- return null;
- }
-
- private CharSequence getExistingUpdateOwnerLabel(PackageInfo pkgInfo) {
- try {
- final String packageName = pkgInfo.packageName;
- final InstallSourceInfo sourceInfo = mPackageManager.getInstallSourceInfo(packageName);
- final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName();
- return getApplicationLabel(existingUpdateOwner);
- } catch (NameNotFoundException e) {
- return null;
- }
- }
-
- private CharSequence getApplicationLabel(String packageName) {
- try {
- final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName,
- ApplicationInfoFlags.of(0));
- return mPackageManager.getApplicationLabel(appInfo);
- } catch (NameNotFoundException e) {
- return null;
- }
- }
-
- private boolean isAppUpdating(PackageInfo newPkgInfo) {
- String pkgName = newPkgInfo.packageName;
- // Check if there is already a package on the device with this name
- // but it has been renamed to something else.
- String[] oldName = mPackageManager.canonicalToCurrentPackageNames(new String[]{pkgName});
- if (oldName != null && oldName.length > 0 && oldName[0] != null) {
- pkgName = oldName[0];
- newPkgInfo.packageName = pkgName;
- newPkgInfo.applicationInfo.packageName = pkgName;
- }
- // Check if package is already installed. display confirmation dialog if replacing pkg
- try {
- // This is a little convoluted because we want to get all uninstalled
- // apps, but this may include apps with just data, and if it is just
- // data we still want to count it as "installed".
- ApplicationInfo appInfo = mPackageManager.getApplicationInfo(pkgName,
- PackageManager.MATCH_UNINSTALLED_PACKAGES);
- if ((appInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
- return false;
- }
- } catch (NameNotFoundException e) {
- return false;
- }
- return true;
- }
-
- /**
- * Once the user returns from Settings related to installing from unknown sources, reattempt
- * the installation if the source app is granted permission to install other apps. Abort the
- * installation if the source app is still not granted installing permission.
- * @return {@link InstallUserActionRequired} containing data required to ask user confirmation
- * to proceed with the install.
- * {@link InstallAborted} if there was an error while recomputing, or the source still
- * doesn't have install permission.
- */
- public InstallStage reattemptInstall() {
- InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo);
- if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) {
- // Source app now has appOp granted.
- return generateConfirmationSnippet();
- } else if (unknownSourceStage.getStageCode() == InstallStage.STAGE_ABORTED) {
- // There was some error in determining the AppOp code for the source app.
- // Abort installation
- return unknownSourceStage;
- } else {
- // AppOpsManager again returned a MODE_ERRORED or MODE_DEFAULT op code. This was
- // unexpected while reattempting the install. Let's abort it.
- Log.e(TAG, "AppOp still not granted.");
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- }
-
- private InstallStage handleUnknownSources(AppOpRequestInfo requestInfo) {
- if (requestInfo.getCallingPackage() == null) {
- Log.i(TAG, "No source found for package " + mNewPackageInfo.packageName);
- return new InstallUserActionRequired.Builder(
- USER_ACTION_REASON_ANONYMOUS_SOURCE, null)
- .build();
- }
- // Shouldn't use static constant directly, see b/65534401.
- final String appOpStr =
- AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
- final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr,
- requestInfo.getOriginatingUid(),
- requestInfo.getCallingPackage(), requestInfo.getAttributionTag(),
- "Started package installation activity");
-
- if (mLocalLOGV) {
- Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode);
- }
- switch (appOpMode) {
- case AppOpsManager.MODE_DEFAULT:
- mAppOpsManager.setMode(appOpStr, requestInfo.getOriginatingUid(),
- requestInfo.getCallingPackage(), AppOpsManager.MODE_ERRORED);
- // fall through
- case AppOpsManager.MODE_ERRORED:
- try {
- ApplicationInfo sourceInfo =
- mPackageManager.getApplicationInfo(requestInfo.getCallingPackage(), 0);
- AppSnippet sourceAppSnippet = getAppSnippet(mContext, sourceInfo);
- return new InstallUserActionRequired.Builder(
- USER_ACTION_REASON_UNKNOWN_SOURCE, sourceAppSnippet)
- .setDialogMessage(requestInfo.getCallingPackage())
- .build();
- } catch (NameNotFoundException e) {
- Log.e(TAG, "Did not find appInfo for " + requestInfo.getCallingPackage());
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- case AppOpsManager.MODE_ALLOWED:
- return new InstallReady();
- default:
- Log.e(TAG, "Invalid app op mode " + appOpMode
- + " for OP_REQUEST_INSTALL_PACKAGES found for uid "
- + requestInfo.getOriginatingUid());
- return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
- }
- }
-
-
- /**
- * Kick off the installation. Register a broadcast listener to get the result of the
- * installation and commit the staged session here. If the installation was session based,
- * signal the PackageInstaller that the user has granted permission to proceed with the install
- */
- public void initiateInstall() {
- if (mSessionId > 0) {
- mPackageInstaller.setPermissionsResult(mSessionId, true);
- mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_DONE)
- .setActivityResultCode(Activity.RESULT_OK).build());
- return;
- }
-
- Uri uri = mIntent.getData();
- if (uri != null && SCHEME_PACKAGE.equals(uri.getScheme())) {
- try {
- mPackageManager.installExistingPackage(mNewPackageInfo.packageName);
- setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null, -1);
- } catch (PackageManager.NameNotFoundException e) {
- setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
- PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
- }
- return;
- }
-
- if (mStagedSessionId <= 0) {
- // How did we even land here?
- Log.e(TAG, "Invalid local session and caller initiated session");
- mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
- .build());
- return;
- }
-
- int installId;
- try {
- mInstallResult.setValue(new InstallInstalling(mAppSnippet));
- installId = InstallEventReceiver.addObserver(mContext,
- EventResultPersister.GENERATE_NEW_ID, this::setStageBasedOnResult);
- } catch (EventResultPersister.OutOfIdsException e) {
- setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
- PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
- return;
- }
-
- Intent broadcastIntent = new Intent(BROADCAST_ACTION);
- broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- broadcastIntent.setPackage(mContext.getPackageName());
- broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, installId);
-
- PendingIntent pendingIntent = PendingIntent.getBroadcast(
- mContext, installId, broadcastIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
-
- try {
- PackageInstaller.Session session = mPackageInstaller.openSession(mStagedSessionId);
- session.commit(pendingIntent.getIntentSender());
- } catch (Exception e) {
- Log.e(TAG, "Session " + mStagedSessionId + " could not be opened.", e);
- mPackageInstaller.abandonSession(mStagedSessionId);
- setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
- PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
- }
- }
-
- private void setStageBasedOnResult(int statusCode, int legacyStatus, String message,
- int serviceId) {
- if (statusCode == PackageInstaller.STATUS_SUCCESS) {
- boolean shouldReturnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
-
- InstallSuccess.Builder successBuilder = new InstallSuccess.Builder(mAppSnippet)
- .setShouldReturnResult(shouldReturnResult);
- Intent resultIntent;
- if (shouldReturnResult) {
- resultIntent = new Intent()
- .putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED);
- } else {
- resultIntent = mPackageManager
- .getLaunchIntentForPackage(mNewPackageInfo.packageName);
- }
- successBuilder.setResultIntent(resultIntent);
-
- mInstallResult.setValue(successBuilder.build());
- } else {
- mInstallResult.setValue(
- new InstallFailed(mAppSnippet, statusCode, legacyStatus, message));
- }
- }
-
- public MutableLiveData<InstallStage> getInstallResult() {
- return mInstallResult;
- }
-
- /**
- * Cleanup the staged session. Also signal the packageinstaller that an install session is to
- * be aborted
- */
- public void cleanupInstall() {
- if (mSessionId > 0) {
- mPackageInstaller.setPermissionsResult(mSessionId, false);
- } else if (mStagedSessionId > 0) {
- cleanupStagingSession();
- }
- }
-
- /**
- * When the identity of the install source could not be determined, user can skip checking the
- * source and directly proceed with the install.
- */
- public InstallStage forcedSkipSourceCheck() {
- return generateConfirmationSnippet();
- }
-
- public MutableLiveData<Integer> getStagingProgress() {
- if (mSessionStager != null) {
- return mSessionStager.getProgress();
- }
- return new MutableLiveData<>(0);
- }
-
- public MutableLiveData<InstallStage> getStagingResult() {
- return mStagingResult;
- }
-
- public interface SessionStageListener {
-
- void onStagingSuccess(SessionInfo info);
-
- void onStagingFailure();
- }
-
- public static class CallerInfo {
-
- private final String mPackageName;
- private final int mUid;
-
- public CallerInfo(String packageName, int uid) {
- mPackageName = packageName;
- mUid = uid;
- }
-
- public String getPackageName() {
- return mPackageName;
- }
-
- public int getUid() {
- return mUid;
- }
- }
-
- public static class AppOpRequestInfo {
-
- private String mCallingPackage;
- private String mAttributionTag;
- private int mOrginatingUid;
-
- public AppOpRequestInfo(String callingPackage, int orginatingUid, String attributionTag) {
- mCallingPackage = callingPackage;
- mOrginatingUid = orginatingUid;
- mAttributionTag = attributionTag;
- }
-
- public String getCallingPackage() {
- return mCallingPackage;
- }
-
- public String getAttributionTag() {
- return mAttributionTag;
- }
-
- public int getOriginatingUid() {
- return mOrginatingUid;
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
new file mode 100644
index 0000000..326e533
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -0,0 +1,867 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.model
+
+import android.Manifest
+import android.app.Activity
+import android.app.AppOpsManager
+import android.app.PendingIntent
+import android.app.admin.DevicePolicyManager
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageInstaller.SessionInfo
+import android.content.pm.PackageInstaller.SessionParams
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import android.os.Process
+import android.os.UserManager
+import android.text.TextUtils
+import android.util.EventLog
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.packageinstaller.R
+import com.android.packageinstaller.common.EventResultPersister
+import com.android.packageinstaller.common.EventResultPersister.OutOfIdsException
+import com.android.packageinstaller.common.InstallEventReceiver
+import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_DONE
+import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_INTERNAL_ERROR
+import com.android.packageinstaller.v2.model.InstallAborted.Companion.ABORT_REASON_POLICY
+import com.android.packageinstaller.v2.model.InstallAborted.Companion.DLG_PACKAGE_ERROR
+import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_ANONYMOUS_SOURCE
+import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_INSTALL_CONFIRMATION
+import com.android.packageinstaller.v2.model.InstallUserActionRequired.Companion.USER_ACTION_REASON_UNKNOWN_SOURCE
+import com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery
+import com.android.packageinstaller.v2.model.PackageUtil.generateStubPackageInfo
+import com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet
+import com.android.packageinstaller.v2.model.PackageUtil.getPackageInfo
+import com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid
+import com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner
+import com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested
+import com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted
+import java.io.File
+import java.io.IOException
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+class InstallRepository(private val context: Context) {
+
+ private val packageManager: PackageManager = context.packageManager
+ private val packageInstaller: PackageInstaller = packageManager.packageInstaller
+ private val userManager: UserManager? = context.getSystemService(UserManager::class.java)
+ private val devicePolicyManager: DevicePolicyManager? =
+ context.getSystemService(DevicePolicyManager::class.java)
+ private val appOpsManager: AppOpsManager? = context.getSystemService(AppOpsManager::class.java)
+ private val localLOGV = false
+ private var isSessionInstall = false
+ private var isTrustedSource = false
+ private val _stagingResult = MutableLiveData<InstallStage>()
+ val stagingResult: LiveData<InstallStage>
+ get() = _stagingResult
+ private val _installResult = MutableLiveData<InstallStage>()
+ val installResult: LiveData<InstallStage>
+ get() = _installResult
+
+ /**
+ * Session ID for a session created when caller uses PackageInstaller APIs
+ */
+ private var sessionId = SessionInfo.INVALID_ID
+
+ /**
+ * Session ID for a session created by this app
+ */
+ var stagedSessionId = SessionInfo.INVALID_ID
+ private set
+ private var callingUid = Process.INVALID_UID
+ private var callingPackage: String? = null
+ private var sessionStager: SessionStager? = null
+ private lateinit var intent: Intent
+ private lateinit var appOpRequestInfo: AppOpRequestInfo
+ private lateinit var appSnippet: PackageUtil.AppSnippet
+
+ /**
+ * PackageInfo of the app being installed on device.
+ */
+ private var newPackageInfo: PackageInfo? = null
+
+ /**
+ * Extracts information from the incoming install intent, checks caller's permission to install
+ * packages, verifies that the caller is the install session owner (in case of a session based
+ * install) and checks if the current user has restrictions set that prevent app installation,
+ *
+ * @param intent the incoming [Intent] object for installing a package
+ * @param callerInfo [CallerInfo] that holds the callingUid and callingPackageName
+ * @return
+ * * [InstallAborted] if there are errors while performing the checks
+ * * [InstallStaging] after successfully performing the checks
+ */
+ fun performPreInstallChecks(intent: Intent, callerInfo: CallerInfo): InstallStage {
+ this.intent = intent
+
+ var callingAttributionTag: String? = null
+
+ isSessionInstall =
+ PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL == intent.action
+ || PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action
+
+ sessionId = if (isSessionInstall)
+ intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID)
+ else SessionInfo.INVALID_ID
+
+ stagedSessionId = intent.getIntExtra(EXTRA_STAGED_SESSION_ID, SessionInfo.INVALID_ID)
+
+ callingPackage = callerInfo.packageName
+
+ if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) {
+ val sessionInfo: SessionInfo? = packageInstaller.getSessionInfo(sessionId)
+ callingPackage = sessionInfo?.getInstallerPackageName()
+ callingAttributionTag = sessionInfo?.getInstallerAttributionTag()
+ }
+
+ // Uid of the source package, coming from ActivityManager
+ callingUid = callerInfo.uid
+ if (callingUid == Process.INVALID_UID) {
+ Log.e(LOG_TAG, "Could not determine the launching uid.")
+ }
+ val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage)
+ // Uid of the source package, with a preference to uid from ApplicationInfo
+ val originatingUid = sourceInfo?.uid ?: callingUid
+ appOpRequestInfo = AppOpRequestInfo(
+ getPackageNameForUid(context, originatingUid, callingPackage),
+ originatingUid, callingAttributionTag
+ )
+
+ if (callingUid == Process.INVALID_UID && sourceInfo == null) {
+ // Caller's identity could not be determined. Abort the install
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+
+ if ((sessionId != SessionInfo.INVALID_ID
+ && !isCallerSessionOwner(packageInstaller, originatingUid, sessionId))
+ || (stagedSessionId != SessionInfo.INVALID_ID
+ && !isCallerSessionOwner(packageInstaller, Process.myUid(), stagedSessionId))
+ ) {
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+
+ isTrustedSource = isInstallRequestFromTrustedSource(sourceInfo, this.intent, originatingUid)
+ if (!isInstallPermissionGrantedOrRequested(
+ context, callingUid, originatingUid, isTrustedSource
+ )
+ ) {
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+
+ val restriction = getDevicePolicyRestrictions()
+ if (restriction != null) {
+ val adminSupportDetailsIntent =
+ devicePolicyManager!!.createAdminSupportIntent(restriction)
+ return InstallAborted(
+ ABORT_REASON_POLICY, message = restriction, resultIntent = adminSupportDetailsIntent
+ )
+ }
+
+ maybeRemoveInvalidInstallerPackageName(callerInfo)
+
+ return InstallStaging()
+ }
+
+ /**
+ * @return the ApplicationInfo for the installation source (the calling package), if available
+ */
+ private fun getSourceInfo(callingPackage: String?): ApplicationInfo? {
+ return try {
+ callingPackage?.let { packageManager.getApplicationInfo(it, 0) }
+ } catch (ignored: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ private fun isInstallRequestFromTrustedSource(
+ sourceInfo: ApplicationInfo?,
+ intent: Intent,
+ originatingUid: Int,
+ ): Boolean {
+ val isNotUnknownSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)
+ return (sourceInfo != null && sourceInfo.isPrivilegedApp
+ && (isNotUnknownSource
+ || isPermissionGranted(context, Manifest.permission.INSTALL_PACKAGES, originatingUid)))
+ }
+
+ private fun getDevicePolicyRestrictions(): String? {
+ val restrictions = arrayOf(
+ UserManager.DISALLOW_INSTALL_APPS,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY
+ )
+ for (restriction in restrictions) {
+ if (!userManager!!.hasUserRestrictionForUser(restriction, Process.myUserHandle())) {
+ continue
+ }
+ return restriction
+ }
+ return null
+ }
+
+ private fun maybeRemoveInvalidInstallerPackageName(callerInfo: CallerInfo) {
+ val installerPackageNameFromIntent =
+ intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME) ?: return
+
+ if (!TextUtils.equals(installerPackageNameFromIntent, callerInfo.packageName)
+ && callerInfo.packageName != null
+ && isPermissionGranted(
+ packageManager, Manifest.permission.INSTALL_PACKAGES, callerInfo.packageName
+ )
+ ) {
+ Log.e(
+ LOG_TAG, "The given installer package name $installerPackageNameFromIntent"
+ + " is invalid. Remove it."
+ )
+ EventLog.writeEvent(
+ 0x534e4554, "236687884", callerInfo.uid,
+ "Invalid EXTRA_INSTALLER_PACKAGE_NAME"
+ )
+ intent.removeExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ fun stageForInstall() {
+ val uri = intent.data
+ if (stagedSessionId != SessionInfo.INVALID_ID
+ || isSessionInstall
+ || (uri != null && SCHEME_PACKAGE == uri.scheme)
+ ) {
+ // For a session based install or installing with a package:// URI, there is no file
+ // for us to stage.
+ _stagingResult.value = InstallReady()
+ return
+ }
+ if (uri != null
+ && ContentResolver.SCHEME_CONTENT == uri.scheme
+ && canPackageQuery(context, callingUid, uri)
+ ) {
+ if (stagedSessionId > 0) {
+ val info: SessionInfo? = packageInstaller.getSessionInfo(stagedSessionId)
+ if (info == null || !info.isActive || info.resolvedBaseApkPath == null) {
+ Log.w(LOG_TAG, "Session $stagedSessionId in funky state; ignoring")
+ if (info != null) {
+ cleanupStagingSession()
+ }
+ stagedSessionId = 0
+ }
+ }
+
+ // Session does not exist, or became invalid.
+ if (stagedSessionId <= 0) {
+ // Create session here to be able to show error.
+ try {
+ context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd ->
+ val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor
+ val params: SessionParams =
+ createSessionParams(intent, pfd, uri.toString())
+ stagedSessionId = packageInstaller.createSession(params)
+ }
+ } catch (e: IOException) {
+ Log.w(LOG_TAG, "Failed to create a staging session", e)
+ _stagingResult.value = InstallAborted(
+ ABORT_REASON_INTERNAL_ERROR,
+ resultIntent = Intent().putExtra(
+ Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK
+ ),
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ return
+ }
+ }
+
+ sessionStager = SessionStager(context, uri, stagedSessionId)
+ GlobalScope.launch(Dispatchers.Main) {
+ val wasFileStaged = sessionStager!!.execute()
+
+ if (wasFileStaged) {
+ _stagingResult.value = InstallReady()
+ } else {
+ cleanupStagingSession()
+ _stagingResult.value = InstallAborted(
+ ABORT_REASON_INTERNAL_ERROR,
+ resultIntent = Intent().putExtra(
+ Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK
+ ),
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ }
+ }
+ }
+ }
+
+ private fun cleanupStagingSession() {
+ if (stagedSessionId > 0) {
+ try {
+ packageInstaller.abandonSession(stagedSessionId)
+ } catch (ignored: SecurityException) {
+ }
+ stagedSessionId = 0
+ }
+ }
+
+ private fun createSessionParams(
+ intent: Intent,
+ pfd: ParcelFileDescriptor?,
+ debugPathName: String,
+ ): SessionParams {
+ val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
+ val referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER, Uri::class.java)
+ params.setPackageSource(
+ if (referrerUri != null)
+ PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
+ else PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE
+ )
+ params.setInstallAsInstantApp(false)
+ params.setReferrerUri(referrerUri)
+ params.setOriginatingUri(
+ intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java)
+ )
+ params.setOriginatingUid(
+ intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID)
+ )
+ params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME))
+ params.setInstallReason(PackageManager.INSTALL_REASON_USER)
+ // Disable full screen intent usage by for sideloads.
+ params.setPermissionState(
+ Manifest.permission.USE_FULL_SCREEN_INTENT, SessionParams.PERMISSION_STATE_DENIED
+ )
+ if (pfd != null) {
+ try {
+ val installInfo = packageInstaller.readInstallInfo(pfd, debugPathName, 0)
+ params.setAppPackageName(installInfo.packageName)
+ params.setInstallLocation(installInfo.installLocation)
+ params.setSize(installInfo.calculateInstalledSize(params, pfd))
+ } catch (e: PackageInstaller.PackageParsingException) {
+ Log.e(LOG_TAG, "Cannot parse package $debugPathName. Assuming defaults.", e)
+ params.setSize(pfd.statSize)
+ } catch (e: IOException) {
+ Log.e(LOG_TAG, "Cannot calculate installed size $debugPathName. " +
+ "Try only apk size.", e
+ )
+ }
+ } else {
+ Log.e(LOG_TAG, "Cannot parse package $debugPathName. Assuming defaults.")
+ }
+ return params
+ }
+
+ /**
+ * Processes Install session, file:// or package:// URI to generate data pertaining to user
+ * confirmation for an install. This method also checks if the source app has the AppOp granted
+ * to install unknown apps. If an AppOp is to be requested, cache the user action prompt data to
+ * be reused once appOp has been granted
+ *
+ * @return
+ * * [InstallAborted]
+ * * If install session is invalid (not sealed or resolvedBaseApk path is invalid)
+ * * Source app doesn't have visibility to target app
+ * * The APK is invalid
+ * * URI is invalid
+ * * Can't get ApplicationInfo for source app, to request AppOp
+ *
+ * * [InstallUserActionRequired]
+ * * If AppOP is granted and user action is required to proceed with install
+ * * If AppOp grant is to be requested from the user
+ */
+ fun requestUserConfirmation(): InstallStage {
+ return if (isTrustedSource) {
+ if (localLOGV) {
+ Log.i(LOG_TAG, "install allowed")
+ }
+ // Returns InstallUserActionRequired stage if install details could be successfully
+ // computed, else it returns InstallAborted.
+ generateConfirmationSnippet()
+ } else {
+ val unknownSourceStage = handleUnknownSources(appOpRequestInfo)
+ if (unknownSourceStage.stageCode == InstallStage.STAGE_READY) {
+ // Source app already has appOp granted.
+ generateConfirmationSnippet()
+ } else {
+ unknownSourceStage
+ }
+ }
+ }
+
+ private fun generateConfirmationSnippet(): InstallStage {
+ val packageSource: Any?
+ val pendingUserActionReason: Int
+
+ if (PackageInstaller.ACTION_CONFIRM_INSTALL == intent.action) {
+ val info = packageInstaller.getSessionInfo(sessionId)
+ val resolvedPath = info?.resolvedBaseApkPath
+ if (info == null || !info.isSealed || resolvedPath == null) {
+ Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring")
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ packageSource = Uri.fromFile(File(resolvedPath))
+ // TODO: Not sure where is this used yet. PIA.java passes it to
+ // InstallInstalling if not null
+ // mOriginatingURI = null;
+ // mReferrerURI = null;
+ pendingUserActionReason = info.getPendingUserActionReason()
+ } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL == intent.action) {
+ val info = packageInstaller.getSessionInfo(sessionId)
+ if (info == null || !info.isPreApprovalRequested) {
+ Log.w(LOG_TAG, "Session $sessionId in funky state; ignoring")
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ packageSource = info
+ // mOriginatingURI = null;
+ // mReferrerURI = null;
+ pendingUserActionReason = info.getPendingUserActionReason()
+ } else {
+ // Two possible origins:
+ // 1. Installation with SCHEME_PACKAGE.
+ // 2. Installation with "file://" for session created by this app
+ packageSource =
+ if (intent.data?.scheme == SCHEME_PACKAGE) {
+ intent.data
+ } else {
+ val stagedSessionInfo = packageInstaller.getSessionInfo(stagedSessionId)
+ Uri.fromFile(File(stagedSessionInfo?.resolvedBaseApkPath!!))
+ }
+ // mOriginatingURI = mIntent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
+ // mReferrerURI = mIntent.getParcelableExtra(Intent.EXTRA_REFERRER);
+ pendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE
+ }
+
+ // if there's nothing to do, quietly slip into the ether
+ if (packageSource == null) {
+ Log.w(LOG_TAG, "Unspecified source")
+ return InstallAborted(
+ ABORT_REASON_INTERNAL_ERROR,
+ resultIntent = Intent().putExtra(
+ Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_URI
+ ),
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ }
+ return processAppSnippet(packageSource, pendingUserActionReason)
+ }
+
+ /**
+ * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install
+ * session) to set up the installer for this install.
+ *
+ * @param source The source of package URI or SessionInfo
+ * @return
+ * * [InstallUserActionRequired] if source could be processed
+ * * [InstallAborted] if source is invalid or there was an error is processing a source
+ */
+ private fun processAppSnippet(source: Any, userActionReason: Int): InstallStage {
+ return when (source) {
+ is Uri -> processPackageUri(source, userActionReason)
+ is SessionInfo -> processSessionInfo(source, userActionReason)
+ else -> InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ }
+
+ /**
+ * Parse the Uri and set up the installer for this package.
+ *
+ * @param packageUri The URI to parse
+ * @return
+ * * [InstallUserActionRequired] if source could be processed
+ * * [InstallAborted] if source is invalid or there was an error is processing a source
+ */
+ private fun processPackageUri(packageUri: Uri, userActionReason: Int): InstallStage {
+ val scheme = packageUri.scheme
+ val packageName = packageUri.schemeSpecificPart
+ if (scheme == null) {
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ if (localLOGV) {
+ Log.i(LOG_TAG, "processPackageUri(): uri = $packageUri, scheme = $scheme")
+ }
+ when (scheme) {
+ SCHEME_PACKAGE -> {
+ for (handle in userManager!!.getUserHandles(true)) {
+ val pmForUser = context.createContextAsUser(handle, 0).packageManager
+ try {
+ if (pmForUser.canPackageQuery(callingPackage!!, packageName)) {
+ newPackageInfo = pmForUser.getPackageInfo(
+ packageName,
+ PackageManager.GET_PERMISSIONS
+ or PackageManager.MATCH_UNINSTALLED_PACKAGES
+ )
+ }
+ } catch (ignored: PackageManager.NameNotFoundException) {
+ }
+ }
+ if (newPackageInfo == null) {
+ Log.w(
+ LOG_TAG, "Requested package " + packageUri.schemeSpecificPart
+ + " not available. Discontinuing installation"
+ )
+ return InstallAborted(
+ ABORT_REASON_INTERNAL_ERROR,
+ errorDialogType = DLG_PACKAGE_ERROR,
+ resultIntent = Intent().putExtra(
+ Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_FAILED_INVALID_APK
+ ),
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ }
+ appSnippet = getAppSnippet(context, newPackageInfo!!)
+ if (localLOGV) {
+ Log.i(LOG_TAG, "Created snippet for " + appSnippet.label)
+ }
+ }
+
+ ContentResolver.SCHEME_FILE -> {
+ val sourceFile = packageUri.path?.let { File(it) }
+ newPackageInfo = sourceFile?.let {
+ getPackageInfo(context, it, PackageManager.GET_PERMISSIONS)
+ }
+
+ // Check for parse errors
+ if (newPackageInfo == null) {
+ Log.w(
+ LOG_TAG, "Parse error when parsing manifest. " +
+ "Discontinuing installation"
+ )
+ return InstallAborted(
+ ABORT_REASON_INTERNAL_ERROR,
+ errorDialogType = DLG_PACKAGE_ERROR,
+ resultIntent = Intent().putExtra(
+ Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_APK
+ ),
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ }
+ if (localLOGV) {
+ Log.i(LOG_TAG, "Creating snippet for local file $sourceFile")
+ }
+ appSnippet = getAppSnippet(context, newPackageInfo!!, sourceFile!!)
+ }
+
+ else -> {
+ Log.e(LOG_TAG, "Unexpected URI scheme $packageUri")
+ return InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ }
+ return InstallUserActionRequired(
+ USER_ACTION_REASON_INSTALL_CONFIRMATION, appSnippet, isAppUpdating(newPackageInfo!!),
+ getUpdateMessage(newPackageInfo!!, userActionReason)
+ )
+ }
+
+ /**
+ * Use the SessionInfo and set up the installer for pre-commit install session.
+ *
+ * @param sessionInfo The SessionInfo to compose
+ * @return
+ * * [InstallUserActionRequired] if source could be processed
+ * * [InstallAborted] if source is invalid or there was an error is processing a source
+ */
+ private fun processSessionInfo(sessionInfo: SessionInfo, userActionReason: Int): InstallStage {
+ newPackageInfo = generateStubPackageInfo(sessionInfo.getAppPackageName())
+ appSnippet = getAppSnippet(context, sessionInfo)
+
+ return InstallUserActionRequired(
+ USER_ACTION_REASON_INSTALL_CONFIRMATION, appSnippet, isAppUpdating(newPackageInfo!!),
+ getUpdateMessage(newPackageInfo!!, userActionReason)
+
+ )
+ }
+
+ private fun getUpdateMessage(pkgInfo: PackageInfo, userActionReason: Int): String? {
+ if (isAppUpdating(pkgInfo)) {
+ val existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo)
+ val requestedUpdateOwnerLabel = getApplicationLabel(callingPackage)
+ if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
+ && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP
+ ) {
+ return context.getString(
+ R.string.install_confirm_question_update_owner_reminder,
+ requestedUpdateOwnerLabel, existingUpdateOwnerLabel
+ )
+ }
+ }
+ return null
+ }
+
+ private fun getExistingUpdateOwnerLabel(pkgInfo: PackageInfo): CharSequence? {
+ return try {
+ val packageName = pkgInfo.packageName
+ val sourceInfo = packageManager.getInstallSourceInfo(packageName)
+ val existingUpdateOwner = sourceInfo.updateOwnerPackageName
+ getApplicationLabel(existingUpdateOwner)
+ } catch (e: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ private fun getApplicationLabel(packageName: String?): CharSequence? {
+ return try {
+ val appInfo = packageName?.let {
+ packageManager.getApplicationInfo(
+ it, PackageManager.ApplicationInfoFlags.of(0)
+ )
+ }
+ appInfo?.let { packageManager.getApplicationLabel(it) }
+ } catch (e: PackageManager.NameNotFoundException) {
+ null
+ }
+ }
+
+ private fun isAppUpdating(newPkgInfo: PackageInfo): Boolean {
+ var pkgName = newPkgInfo.packageName
+ // Check if there is already a package on the device with this name
+ // but it has been renamed to something else.
+ val oldName = packageManager.canonicalToCurrentPackageNames(arrayOf(pkgName))
+ if (oldName != null && oldName.isNotEmpty() && oldName[0] != null) {
+ pkgName = oldName[0]
+ newPkgInfo.packageName = pkgName
+ newPkgInfo.applicationInfo?.packageName = pkgName
+ }
+
+ // Check if package is already installed. display confirmation dialog if replacing pkg
+ try {
+ // This is a little convoluted because we want to get all uninstalled
+ // apps, but this may include apps with just data, and if it is just
+ // data we still want to count it as "installed".
+ val appInfo = packageManager.getApplicationInfo(
+ pkgName, PackageManager.MATCH_UNINSTALLED_PACKAGES
+ )
+ if (appInfo.flags and ApplicationInfo.FLAG_INSTALLED == 0) {
+ return false
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Once the user returns from Settings related to installing from unknown sources, reattempt
+ * the installation if the source app is granted permission to install other apps. Abort the
+ * installation if the source app is still not granted installing permission.
+ *
+ * @return
+ * * [InstallUserActionRequired] containing data required to ask user confirmation
+ * to proceed with the install.
+ * * [InstallAborted] if there was an error while recomputing, or the source still
+ * doesn't have install permission.
+ */
+ fun reattemptInstall(): InstallStage {
+ val unknownSourceStage = handleUnknownSources(appOpRequestInfo)
+ return when (unknownSourceStage.stageCode) {
+ InstallStage.STAGE_READY -> {
+ // Source app now has appOp granted.
+ generateConfirmationSnippet()
+ }
+
+ InstallStage.STAGE_ABORTED -> {
+ // There was some error in determining the AppOp code for the source app.
+ // Abort installation
+ unknownSourceStage
+ }
+
+ else -> {
+ // AppOpsManager again returned a MODE_ERRORED or MODE_DEFAULT op code. This was
+ // unexpected while reattempting the install. Let's abort it.
+ Log.e(LOG_TAG, "AppOp still not granted.")
+ InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ }
+ }
+
+ private fun handleUnknownSources(requestInfo: AppOpRequestInfo): InstallStage {
+ if (requestInfo.callingPackage == null) {
+ Log.i(LOG_TAG, "No source found for package " + newPackageInfo?.packageName)
+ return InstallUserActionRequired(USER_ACTION_REASON_ANONYMOUS_SOURCE)
+ }
+ // Shouldn't use static constant directly, see b/65534401.
+ val appOpStr = AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES)
+ val appOpMode = appOpsManager!!.noteOpNoThrow(
+ appOpStr!!, requestInfo.originatingUid, requestInfo.callingPackage,
+ requestInfo.attributionTag, "Started package installation activity"
+ )
+ if (localLOGV) {
+ Log.i(LOG_TAG, "handleUnknownSources(): appMode=$appOpMode")
+ }
+
+ return when (appOpMode) {
+ AppOpsManager.MODE_DEFAULT, AppOpsManager.MODE_ERRORED -> {
+ if (appOpMode == AppOpsManager.MODE_DEFAULT) {
+ appOpsManager.setMode(
+ appOpStr, requestInfo.originatingUid, requestInfo.callingPackage,
+ AppOpsManager.MODE_ERRORED
+ )
+ }
+ try {
+ val sourceInfo =
+ packageManager.getApplicationInfo(requestInfo.callingPackage, 0)
+ val sourceAppSnippet = getAppSnippet(context, sourceInfo)
+ InstallUserActionRequired(
+ USER_ACTION_REASON_UNKNOWN_SOURCE, appSnippet = sourceAppSnippet,
+ dialogMessage = requestInfo.callingPackage
+ )
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(LOG_TAG, "Did not find appInfo for " + requestInfo.callingPackage)
+ InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ }
+
+ AppOpsManager.MODE_ALLOWED -> InstallReady()
+
+ else -> {
+ Log.e(
+ LOG_TAG, "Invalid app op mode $appOpMode for " +
+ "OP_REQUEST_INSTALL_PACKAGES found for uid $requestInfo.originatingUid"
+ )
+ InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ }
+ }
+ }
+
+ /**
+ * Kick off the installation. Register a broadcast listener to get the result of the
+ * installation and commit the staged session here. If the installation was session based,
+ * signal the PackageInstaller that the user has granted permission to proceed with the install
+ */
+ fun initiateInstall() {
+ if (sessionId > 0) {
+ packageInstaller.setPermissionsResult(sessionId, true)
+ _installResult.value = InstallAborted(
+ ABORT_REASON_DONE, activityResultCode = Activity.RESULT_OK
+ )
+ return
+ }
+ val uri = intent.data
+ if (SCHEME_PACKAGE == uri?.scheme) {
+ try {
+ packageManager.installExistingPackage(
+ newPackageInfo!!.packageName, PackageManager.INSTALL_REASON_USER
+ )
+ setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null)
+ } catch (e: PackageManager.NameNotFoundException) {
+ setStageBasedOnResult(
+ PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
+ null)
+ }
+ return
+ }
+ if (stagedSessionId <= 0) {
+ // How did we even land here?
+ Log.e(LOG_TAG, "Invalid local session and caller initiated session")
+ _installResult.value = InstallAborted(ABORT_REASON_INTERNAL_ERROR)
+ return
+ }
+ val installId: Int
+ try {
+ _installResult.value = InstallInstalling(appSnippet)
+ installId = InstallEventReceiver.addObserver(
+ context, EventResultPersister.GENERATE_NEW_ID
+ ) { statusCode: Int, legacyStatus: Int, message: String?, serviceId: Int ->
+ setStageBasedOnResult(statusCode, legacyStatus, message)
+ }
+ } catch (e: OutOfIdsException) {
+ setStageBasedOnResult(
+ PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null)
+ return
+ }
+ val broadcastIntent = Intent(BROADCAST_ACTION)
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ broadcastIntent.setPackage(context.packageName)
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, installId)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context, installId, broadcastIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ try {
+ val session = packageInstaller.openSession(stagedSessionId)
+ session.commit(pendingIntent.intentSender)
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "Session $stagedSessionId could not be opened.", e)
+ packageInstaller.abandonSession(stagedSessionId)
+ setStageBasedOnResult(
+ PackageInstaller.STATUS_FAILURE, PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null)
+ }
+ }
+
+ private fun setStageBasedOnResult(
+ statusCode: Int,
+ legacyStatus: Int,
+ message: String?
+ ) {
+ if (statusCode == PackageInstaller.STATUS_SUCCESS) {
+ val shouldReturnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
+ val resultIntent = if (shouldReturnResult) {
+ Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED)
+ } else {
+ packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName)
+ }
+ _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent))
+ } else {
+ _installResult.setValue(InstallFailed(appSnippet, statusCode, legacyStatus, message))
+ }
+ }
+
+ /**
+ * Cleanup the staged session. Also signal the packageinstaller that an install session is to
+ * be aborted
+ */
+ fun cleanupInstall() {
+ if (sessionId > 0) {
+ packageInstaller.setPermissionsResult(sessionId, false)
+ } else if (stagedSessionId > 0) {
+ cleanupStagingSession()
+ }
+ }
+
+ /**
+ * When the identity of the install source could not be determined, user can skip checking the
+ * source and directly proceed with the install.
+ */
+ fun forcedSkipSourceCheck(): InstallStage {
+ return generateConfirmationSnippet()
+ }
+
+ val stagingProgress: LiveData<Int>
+ get() = sessionStager?.progress ?: MutableLiveData(0)
+
+ companion object {
+ const val EXTRA_STAGED_SESSION_ID = "com.android.packageinstaller.extra.STAGED_SESSION_ID"
+ const val SCHEME_PACKAGE = "package"
+ const val BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT"
+ private val LOG_TAG = InstallRepository::class.java.simpleName
+ }
+
+ data class CallerInfo(val packageName: String?, val uid: Int)
+ data class AppOpRequestInfo(
+ val callingPackage: String?,
+ val originatingUid: Int,
+ val attributionTag: String?,
+ )
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
new file mode 100644
index 0000000..be49b39
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallStages.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ * https://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.packageinstaller.v2.model
+
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+
+sealed class InstallStage(val stageCode: Int) {
+
+ companion object {
+ const val STAGE_DEFAULT = -1
+ const val STAGE_ABORTED = 0
+ const val STAGE_STAGING = 1
+ const val STAGE_READY = 2
+ const val STAGE_USER_ACTION_REQUIRED = 3
+ const val STAGE_INSTALLING = 4
+ const val STAGE_SUCCESS = 5
+ const val STAGE_FAILED = 6
+ }
+}
+
+class InstallStaging : InstallStage(STAGE_STAGING)
+
+class InstallReady : InstallStage(STAGE_READY)
+
+data class InstallUserActionRequired(
+ val actionReason: Int,
+ private val appSnippet: PackageUtil.AppSnippet? = null,
+ val isAppUpdating: Boolean = false,
+ val dialogMessage: String? = null,
+) : InstallStage(STAGE_USER_ACTION_REQUIRED) {
+
+ val appIcon: Drawable?
+ get() = appSnippet?.icon
+
+ val appLabel: String?
+ get() = appSnippet?.let { appSnippet.label as String? }
+
+ companion object {
+ const val USER_ACTION_REASON_UNKNOWN_SOURCE = 0
+ const val USER_ACTION_REASON_ANONYMOUS_SOURCE = 1
+ const val USER_ACTION_REASON_INSTALL_CONFIRMATION = 2
+ }
+}
+
+data class InstallInstalling(private val appSnippet: PackageUtil.AppSnippet) :
+ InstallStage(STAGE_INSTALLING) {
+
+ val appIcon: Drawable?
+ get() = appSnippet.icon
+
+ val appLabel: String?
+ get() = appSnippet.label as String?
+}
+
+data class InstallSuccess(
+ private val appSnippet: PackageUtil.AppSnippet,
+ val shouldReturnResult: Boolean = false,
+ /**
+ *
+ * * If the caller is requesting a result back, this will hold the Intent with
+ * [Intent.EXTRA_INSTALL_RESULT] set to [PackageManager.INSTALL_SUCCEEDED] which is sent
+ * back to the caller.
+ *
+ * * If the caller doesn't want the result back, this will hold the Intent that launches
+ * the newly installed / updated app if a launchable activity exists.
+ */
+ val resultIntent: Intent? = null,
+) : InstallStage(STAGE_SUCCESS) {
+
+ val appIcon: Drawable?
+ get() = appSnippet.icon
+
+ val appLabel: String?
+ get() = appSnippet.label as String?
+}
+
+data class InstallFailed(
+ private val appSnippet: PackageUtil.AppSnippet,
+ val legacyCode: Int,
+ val statusCode: Int,
+ val message: String?,
+) : InstallStage(STAGE_FAILED) {
+
+ val appIcon: Drawable?
+ get() = appSnippet.icon
+
+ val appLabel: String?
+ get() = appSnippet.label as String?
+}
+
+data class InstallAborted(
+ val abortReason: Int,
+ /**
+ * It will hold the restriction name, when the restriction was enforced by the system, and not
+ * a device admin.
+ */
+ val message: String? = null,
+ /**
+ * * If abort reason is [ABORT_REASON_POLICY], then this will hold the Intent
+ * to display a support dialog when a feature was disabled by an admin. It will be
+ * `null` if the feature is disabled by the system. In this case, the restriction name
+ * will be set in [message]
+ * * If the abort reason is [ABORT_REASON_INTERNAL_ERROR], it **may** hold an
+ * intent to be sent as a result to the calling activity.
+ */
+ val resultIntent: Intent? = null,
+ val activityResultCode: Int = Activity.RESULT_CANCELED,
+ val errorDialogType: Int? = 0,
+) : InstallStage(STAGE_ABORTED) {
+
+ companion object {
+ const val ABORT_REASON_INTERNAL_ERROR = 0
+ const val ABORT_REASON_POLICY = 1
+ const val ABORT_REASON_DONE = 2
+ const val DLG_PACKAGE_ERROR = 1
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
deleted file mode 100644
index fe05237..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model;
-
-import android.Manifest;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.content.res.Resources;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.util.Log;
-import androidx.annotation.NonNull;
-import java.io.File;
-import java.util.Arrays;
-import java.util.Objects;
-
-public class PackageUtil {
-
- private static final String TAG = InstallRepository.class.getSimpleName();
- private static final String DOWNLOADS_AUTHORITY = "downloads";
- private static final String SPLIT_BASE_APK_END_WITH = "base.apk";
-
- /**
- * Determines if the UID belongs to the system downloads provider and returns the
- * {@link ApplicationInfo} of the provider
- *
- * @param uid UID of the caller
- * @return {@link ApplicationInfo} of the provider if a downloads provider exists, it is a
- * system app, and its UID matches with the passed UID, null otherwise.
- */
- public static ApplicationInfo getSystemDownloadsProviderInfo(PackageManager pm, int uid) {
- final ProviderInfo providerInfo = pm.resolveContentProvider(
- DOWNLOADS_AUTHORITY, 0);
- if (providerInfo == null) {
- // There seems to be no currently enabled downloads provider on the system.
- return null;
- }
- ApplicationInfo appInfo = providerInfo.applicationInfo;
- if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 && uid == appInfo.uid) {
- return appInfo;
- }
- return null;
- }
-
- /**
- * Get the maximum target sdk for a UID.
- *
- * @param context The context to use
- * @param uid The UID requesting the install/uninstall
- * @return The maximum target SDK or -1 if the uid does not match any packages.
- */
- public static int getMaxTargetSdkVersionForUid(@NonNull Context context, int uid) {
- PackageManager pm = context.getPackageManager();
- final String[] packages = pm.getPackagesForUid(uid);
- int targetSdkVersion = -1;
- if (packages != null) {
- for (String packageName : packages) {
- try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
- targetSdkVersion = Math.max(targetSdkVersion, info.targetSdkVersion);
- } catch (PackageManager.NameNotFoundException e) {
- // Ignore and try the next package
- }
- }
- }
- return targetSdkVersion;
- }
-
- public static boolean canPackageQuery(Context context, int callingUid, Uri packageUri) {
- PackageManager pm = context.getPackageManager();
- ProviderInfo info = pm.resolveContentProvider(packageUri.getAuthority(),
- PackageManager.ComponentInfoFlags.of(0));
- if (info == null) {
- return false;
- }
- String targetPackage = info.packageName;
-
- String[] callingPackages = pm.getPackagesForUid(callingUid);
- if (callingPackages == null) {
- return false;
- }
- for (String callingPackage : callingPackages) {
- try {
- if (pm.canPackageQuery(callingPackage, targetPackage)) {
- return true;
- }
- } catch (PackageManager.NameNotFoundException e) {
- // no-op
- }
- }
- return false;
- }
-
- /**
- * @param context the {@link Context} object
- * @param permission the permission name to check
- * @param callingUid the UID of the caller who's permission is being checked
- * @return {@code true} if the callingUid is granted the said permission
- */
- public static boolean isPermissionGranted(Context context, String permission, int callingUid) {
- return context.checkPermission(permission, -1, callingUid)
- == PackageManager.PERMISSION_GRANTED;
- }
-
- /**
- * @param pm the {@link PackageManager} object
- * @param permission the permission name to check
- * @param packageName the name of the package who's permission is being checked
- * @return {@code true} if the package is granted the said permission
- */
- public static boolean isPermissionGranted(PackageManager pm, String permission,
- String packageName) {
- return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED;
- }
-
- /**
- * @param context the {@link Context} object
- * @param callingUid the UID of the caller who's permission is being checked
- * @param originatingUid the UID from where install is being originated. This could be same as
- * callingUid or it will be the UID of the package performing a session based install
- * @param isTrustedSource whether install request is coming from a privileged app or an app that
- * has {@link Manifest.permission.INSTALL_PACKAGES} permission granted
- * @return {@code true} if the package is granted the said permission
- */
- public static boolean isInstallPermissionGrantedOrRequested(Context context, int callingUid,
- int originatingUid, boolean isTrustedSource) {
- boolean isDocumentsManager =
- isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid);
- boolean isSystemDownloadsProvider =
- getSystemDownloadsProviderInfo(context.getPackageManager(), callingUid) != null;
-
- if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
-
- final int targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid);
- if (targetSdkVersion < 0) {
- // Invalid originating uid supplied. Abort install.
- Log.w(TAG, "Cannot get target sdk version for uid " + originatingUid);
- return false;
- } else if (targetSdkVersion >= Build.VERSION_CODES.O
- && !isUidRequestingPermission(context.getPackageManager(), originatingUid,
- Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
- Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission "
- + Manifest.permission.REQUEST_INSTALL_PACKAGES);
- return false;
- }
- }
- return true;
- }
-
- /**
- * @param pm the {@link PackageManager} object
- * @param uid the UID of the caller who's permission is being checked
- * @param permission the permission name to check
- * @return {@code true} if the caller is requesting the said permission in its Manifest
- */
- public static boolean isUidRequestingPermission(PackageManager pm, int uid, String permission) {
- final String[] packageNames = pm.getPackagesForUid(uid);
- if (packageNames == null) {
- return false;
- }
- for (final String packageName : packageNames) {
- final PackageInfo packageInfo;
- try {
- packageInfo = pm.getPackageInfo(packageName,
- PackageManager.GET_PERMISSIONS);
- } catch (PackageManager.NameNotFoundException e) {
- // Ignore and try the next package
- continue;
- }
- if (packageInfo.requestedPermissions != null
- && Arrays.asList(packageInfo.requestedPermissions).contains(permission)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * @param pi the {@link PackageInstaller} object to use
- * @param originatingUid the UID of the package performing a session based install
- * @param sessionId ID of the install session
- * @return {@code true} if the caller is the session owner
- */
- public static boolean isCallerSessionOwner(PackageInstaller pi, int originatingUid,
- int sessionId) {
- if (originatingUid == Process.ROOT_UID) {
- return true;
- }
- PackageInstaller.SessionInfo sessionInfo = pi.getSessionInfo(sessionId);
- if (sessionInfo == null) {
- return false;
- }
- int installerUid = sessionInfo.getInstallerUid();
- return originatingUid == installerUid;
- }
-
- /**
- * Generates a stub {@link PackageInfo} object for the given packageName
- */
- public static PackageInfo generateStubPackageInfo(String packageName) {
- final PackageInfo info = new PackageInfo();
- final ApplicationInfo aInfo = new ApplicationInfo();
- info.applicationInfo = aInfo;
- info.packageName = info.applicationInfo.packageName = packageName;
- return info;
- }
-
- /**
- * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
- * {@link SessionInfo} object
- */
- public static AppSnippet getAppSnippet(Context context, SessionInfo info) {
- PackageManager pm = context.getPackageManager();
- CharSequence label = info.getAppLabel();
- Drawable icon = info.getAppIcon() != null ?
- new BitmapDrawable(context.getResources(), info.getAppIcon())
- : pm.getDefaultActivityIcon();
- return new AppSnippet(label, icon);
- }
-
- /**
- * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
- * {@link PackageInfo} object
- */
- public static AppSnippet getAppSnippet(Context context, PackageInfo pkgInfo) {
- return getAppSnippet(context, pkgInfo.applicationInfo);
- }
-
- /**
- * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
- * {@link ApplicationInfo} object
- */
- public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo) {
- PackageManager pm = context.getPackageManager();
- CharSequence label = pm.getApplicationLabel(appInfo);
- Drawable icon = pm.getApplicationIcon(appInfo);
- return new AppSnippet(label, icon);
- }
-
- /**
- * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
- * supplied APK file
- */
- public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo,
- File sourceFile) {
- ApplicationInfo appInfoFromFile = processAppInfoForFile(appInfo, sourceFile);
- CharSequence label = getAppLabelFromFile(context, appInfoFromFile);
- Drawable icon = getAppIconFromFile(context, appInfoFromFile);
- return new AppSnippet(label, icon);
- }
-
- /**
- * Utility method to load application label
- *
- * @param context context of package that can load the resources
- * @param appInfo ApplicationInfo object of package whose resources are to be loaded
- */
- public static CharSequence getAppLabelFromFile(Context context, ApplicationInfo appInfo) {
- PackageManager pm = context.getPackageManager();
- CharSequence label = null;
- // Try to load the label from the package's resources. If an app has not explicitly
- // specified any label, just use the package name.
- if (appInfo.labelRes != 0) {
- try {
- label = appInfo.loadLabel(pm);
- } catch (Resources.NotFoundException e) {
- }
- }
- if (label == null) {
- label = (appInfo.nonLocalizedLabel != null) ?
- appInfo.nonLocalizedLabel : appInfo.packageName;
- }
- return label;
- }
-
- /**
- * Utility method to load application icon
- *
- * @param context context of package that can load the resources
- * @param appInfo ApplicationInfo object of package whose resources are to be loaded
- */
- public static Drawable getAppIconFromFile(Context context, ApplicationInfo appInfo) {
- PackageManager pm = context.getPackageManager();
- Drawable icon = null;
- // Try to load the icon from the package's resources. If an app has not explicitly
- // specified any resource, just use the default icon for now.
- try {
- if (appInfo.icon != 0) {
- try {
- icon = appInfo.loadIcon(pm);
- } catch (Resources.NotFoundException e) {
- }
- }
- if (icon == null) {
- icon = context.getPackageManager().getDefaultActivityIcon();
- }
- } catch (OutOfMemoryError e) {
- Log.i(TAG, "Could not load app icon", e);
- }
- return icon;
- }
-
- private static ApplicationInfo processAppInfoForFile(ApplicationInfo appInfo, File sourceFile) {
- final String archiveFilePath = sourceFile.getAbsolutePath();
- appInfo.publicSourceDir = archiveFilePath;
-
- if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
- final File[] files = sourceFile.getParentFile().listFiles();
- final String[] splits = Arrays.stream(appInfo.splitNames)
- .map(i -> findFilePath(files, i + ".apk"))
- .filter(Objects::nonNull)
- .toArray(String[]::new);
-
- appInfo.splitSourceDirs = splits;
- appInfo.splitPublicSourceDirs = splits;
- }
- return appInfo;
- }
-
- private static String findFilePath(File[] files, String postfix) {
- for (File file : files) {
- final String path = file.getAbsolutePath();
- if (path.endsWith(postfix)) {
- return path;
- }
- }
- return null;
- }
-
- /**
- * @return the packageName corresponding to a UID.
- */
- public static String getPackageNameForUid(Context context, int sourceUid,
- String callingPackage) {
- if (sourceUid == Process.INVALID_UID) {
- return null;
- }
- // If the sourceUid belongs to the system downloads provider, we explicitly return the
- // name of the Download Manager package. This is because its UID is shared with multiple
- // packages, resulting in uncertainty about which package will end up first in the list
- // of packages associated with this UID
- PackageManager pm = context.getPackageManager();
- ApplicationInfo systemDownloadProviderInfo = getSystemDownloadsProviderInfo(
- pm, sourceUid);
- if (systemDownloadProviderInfo != null) {
- return systemDownloadProviderInfo.packageName;
- }
- String[] packagesForUid = pm.getPackagesForUid(sourceUid);
- if (packagesForUid == null) {
- return null;
- }
- if (packagesForUid.length > 1) {
- if (callingPackage != null) {
- for (String packageName : packagesForUid) {
- if (packageName.equals(callingPackage)) {
- return packageName;
- }
- }
- }
- Log.i(TAG, "Multiple packages found for source uid " + sourceUid);
- }
- return packagesForUid[0];
- }
-
- /**
- * Utility method to get package information for a given {@link File}
- */
- public static PackageInfo getPackageInfo(Context context, File sourceFile, int flags) {
- String filePath = sourceFile.getAbsolutePath();
- if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) {
- File dir = sourceFile.getParentFile();
- if (dir.listFiles().length > 1) {
- // split apks, use file directory to get archive info
- filePath = dir.getPath();
- }
- }
- try {
- return context.getPackageManager().getPackageArchiveInfo(filePath, flags);
- } catch (Exception ignored) {
- return null;
- }
- }
-
- /**
- * Is a profile part of a user?
- *
- * @param userManager The user manager
- * @param userHandle The handle of the user
- * @param profileHandle The handle of the profile
- *
- * @return If the profile is part of the user or the profile parent of the user
- */
- public static boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle,
- UserHandle profileHandle) {
- if (userHandle.equals(profileHandle)) {
- return true;
- }
- return userManager.getProfileParent(profileHandle) != null
- && userManager.getProfileParent(profileHandle).equals(userHandle);
- }
-
- /**
- * The class to hold an incoming package's icon and label.
- * See {@link #getAppSnippet(Context, SessionInfo)},
- * {@link #getAppSnippet(Context, PackageInfo)},
- * {@link #getAppSnippet(Context, ApplicationInfo)},
- * {@link #getAppSnippet(Context, ApplicationInfo, File)}
- */
- public static class AppSnippet {
-
- private CharSequence mLabel;
- private Drawable mIcon;
-
- public AppSnippet(CharSequence label, Drawable icon) {
- mLabel = label;
- mIcon = icon;
- }
-
- public AppSnippet() {
- }
-
- public CharSequence getLabel() {
- return mLabel;
- }
-
- public void setLabel(CharSequence mLabel) {
- this.mLabel = mLabel;
- }
-
- public Drawable getIcon() {
- return mIcon;
- }
-
- public void setIcon(Drawable mIcon) {
- this.mIcon = mIcon;
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt
new file mode 100644
index 0000000..8d8c2f1
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.kt
@@ -0,0 +1,440 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.model
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Build
+import android.os.Process
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import java.io.File
+
+object PackageUtil {
+ private val LOG_TAG = InstallRepository::class.java.simpleName
+ private const val DOWNLOADS_AUTHORITY = "downloads"
+ private const val SPLIT_BASE_APK_END_WITH = "base.apk"
+
+ /**
+ * Determines if the UID belongs to the system downloads provider and returns the
+ * [ApplicationInfo] of the provider
+ *
+ * @param uid UID of the caller
+ * @return [ApplicationInfo] of the provider if a downloads provider exists, it is a
+ * system app, and its UID matches with the passed UID, null otherwise.
+ */
+ private fun getSystemDownloadsProviderInfo(pm: PackageManager, uid: Int): ApplicationInfo? {
+ // Check if there are currently enabled downloads provider on the system.
+ val providerInfo = pm.resolveContentProvider(DOWNLOADS_AUTHORITY, 0)
+ ?: return null
+ val appInfo = providerInfo.applicationInfo
+ return if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) && uid == appInfo.uid) {
+ appInfo
+ } else null
+ }
+
+ /**
+ * Get the maximum target sdk for a UID.
+ *
+ * @param context The context to use
+ * @param uid The UID requesting the install/uninstall
+ * @return The maximum target SDK or -1 if the uid does not match any packages.
+ */
+ @JvmStatic
+ fun getMaxTargetSdkVersionForUid(context: Context, uid: Int): Int {
+ val pm = context.packageManager
+ val packages = pm.getPackagesForUid(uid)
+ var targetSdkVersion = -1
+ if (packages != null) {
+ for (packageName in packages) {
+ try {
+ val info = pm.getApplicationInfo(packageName!!, 0)
+ targetSdkVersion = maxOf(targetSdkVersion, info.targetSdkVersion)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // Ignore and try the next package
+ }
+ }
+ }
+ return targetSdkVersion
+ }
+
+ @JvmStatic
+ fun canPackageQuery(context: Context, callingUid: Int, packageUri: Uri): Boolean {
+ val pm = context.packageManager
+ val info = pm.resolveContentProvider(
+ packageUri.authority!!,
+ PackageManager.ComponentInfoFlags.of(0)
+ ) ?: return false
+ val targetPackage = info.packageName
+ val callingPackages = pm.getPackagesForUid(callingUid) ?: return false
+ for (callingPackage in callingPackages) {
+ try {
+ if (pm.canPackageQuery(callingPackage!!, targetPackage)) {
+ return true
+ }
+ } catch (e: PackageManager.NameNotFoundException) {
+ // no-op
+ }
+ }
+ return false
+ }
+
+ /**
+ * @param context the [Context] object
+ * @param permission the permission name to check
+ * @param callingUid the UID of the caller who's permission is being checked
+ * @return `true` if the callingUid is granted the said permission
+ */
+ @JvmStatic
+ fun isPermissionGranted(context: Context, permission: String, callingUid: Int): Boolean {
+ return (context.checkPermission(permission, -1, callingUid)
+ == PackageManager.PERMISSION_GRANTED)
+ }
+
+ /**
+ * @param pm the [PackageManager] object
+ * @param permission the permission name to check
+ * @param packageName the name of the package who's permission is being checked
+ * @return `true` if the package is granted the said permission
+ */
+ @JvmStatic
+ fun isPermissionGranted(pm: PackageManager, permission: String, packageName: String): Boolean {
+ return pm.checkPermission(permission, packageName) == PackageManager.PERMISSION_GRANTED
+ }
+
+ /**
+ * @param context the [Context] object
+ * @param callingUid the UID of the caller who's permission is being checked
+ * @param originatingUid the UID from where install is being originated. This could be same as
+ * callingUid or it will be the UID of the package performing a session based install
+ * @param isTrustedSource whether install request is coming from a privileged app or an app that
+ * has [Manifest.permission.INSTALL_PACKAGES] permission granted
+ * @return `true` if the package is granted the said permission
+ */
+ @JvmStatic
+ fun isInstallPermissionGrantedOrRequested(
+ context: Context,
+ callingUid: Int,
+ originatingUid: Int,
+ isTrustedSource: Boolean,
+ ): Boolean {
+ val isDocumentsManager =
+ isPermissionGranted(context, Manifest.permission.MANAGE_DOCUMENTS, callingUid)
+ val isSystemDownloadsProvider =
+ getSystemDownloadsProviderInfo(context.packageManager, callingUid) != null
+
+ if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager) {
+ val targetSdkVersion = getMaxTargetSdkVersionForUid(context, originatingUid)
+ if (targetSdkVersion < 0) {
+ // Invalid originating uid supplied. Abort install.
+ Log.w(LOG_TAG, "Cannot get target sdk version for uid $originatingUid")
+ return false
+ } else if (targetSdkVersion >= Build.VERSION_CODES.O
+ && !isUidRequestingPermission(
+ context.packageManager, originatingUid,
+ Manifest.permission.REQUEST_INSTALL_PACKAGES
+ )
+ ) {
+ Log.e(
+ LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission "
+ + Manifest.permission.REQUEST_INSTALL_PACKAGES
+ )
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * @param pm the [PackageManager] object
+ * @param uid the UID of the caller who's permission is being checked
+ * @param permission the permission name to check
+ * @return `true` if the caller is requesting the said permission in its Manifest
+ */
+ private fun isUidRequestingPermission(
+ pm: PackageManager,
+ uid: Int,
+ permission: String,
+ ): Boolean {
+ val packageNames = pm.getPackagesForUid(uid) ?: return false
+ for (packageName in packageNames) {
+ val packageInfo: PackageInfo = try {
+ pm.getPackageInfo(packageName!!, PackageManager.GET_PERMISSIONS)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // Ignore and try the next package
+ continue
+ }
+ if (packageInfo.requestedPermissions != null
+ && listOf(*packageInfo.requestedPermissions!!).contains(permission)
+ ) {
+ return true
+ }
+ }
+ return false
+ }
+
+ /**
+ * @param pi the [PackageInstaller] object to use
+ * @param originatingUid the UID of the package performing a session based install
+ * @param sessionId ID of the install session
+ * @return `true` if the caller is the session owner
+ */
+ @JvmStatic
+ fun isCallerSessionOwner(pi: PackageInstaller, originatingUid: Int, sessionId: Int): Boolean {
+ if (originatingUid == Process.ROOT_UID) {
+ return true
+ }
+ val sessionInfo = pi.getSessionInfo(sessionId) ?: return false
+ val installerUid = sessionInfo.getInstallerUid()
+ return originatingUid == installerUid
+ }
+
+ /**
+ * Generates a stub [PackageInfo] object for the given packageName
+ */
+ @JvmStatic
+ fun generateStubPackageInfo(packageName: String?): PackageInfo {
+ val info = PackageInfo()
+ val aInfo = ApplicationInfo()
+ info.applicationInfo = aInfo
+ info.applicationInfo!!.packageName = packageName
+ info.packageName = info.applicationInfo!!.packageName
+ return info
+ }
+
+ /**
+ * Generates an [AppSnippet] containing an appIcon and appLabel from the
+ * [PackageInstaller.SessionInfo] object
+ */
+ @JvmStatic
+ fun getAppSnippet(context: Context, info: PackageInstaller.SessionInfo): AppSnippet {
+ val pm = context.packageManager
+ val label = info.getAppLabel()
+ val icon = if (info.getAppIcon() != null) BitmapDrawable(
+ context.resources,
+ info.getAppIcon()
+ ) else pm.defaultActivityIcon
+ return AppSnippet(label, icon)
+ }
+
+ /**
+ * Generates an [AppSnippet] containing an appIcon and appLabel from the
+ * [PackageInfo] object
+ */
+ @JvmStatic
+ fun getAppSnippet(context: Context, pkgInfo: PackageInfo): AppSnippet {
+ return pkgInfo.applicationInfo?.let { getAppSnippet(context, it) } ?: run {
+ AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon)
+ }
+ }
+
+ /**
+ * Generates an [AppSnippet] containing an appIcon and appLabel from the
+ * [ApplicationInfo] object
+ */
+ @JvmStatic
+ fun getAppSnippet(context: Context, appInfo: ApplicationInfo): AppSnippet {
+ val pm = context.packageManager
+ val label = pm.getApplicationLabel(appInfo)
+ val icon = pm.getApplicationIcon(appInfo)
+ return AppSnippet(label, icon)
+ }
+
+ /**
+ * Generates an [AppSnippet] containing an appIcon and appLabel from the
+ * supplied APK file
+ */
+ @JvmStatic
+ fun getAppSnippet(context: Context, pkgInfo: PackageInfo, sourceFile: File): AppSnippet {
+ pkgInfo.applicationInfo?.let {
+ val appInfoFromFile = processAppInfoForFile(it, sourceFile)
+ val label = getAppLabelFromFile(context, appInfoFromFile)
+ val icon = getAppIconFromFile(context, appInfoFromFile)
+ return AppSnippet(label, icon)
+ } ?: run {
+ return AppSnippet(pkgInfo.packageName, context.packageManager.defaultActivityIcon)
+ }
+ }
+
+ /**
+ * Utility method to load application label
+ *
+ * @param context context of package that can load the resources
+ * @param appInfo ApplicationInfo object of package whose resources are to be loaded
+ */
+ private fun getAppLabelFromFile(context: Context, appInfo: ApplicationInfo): CharSequence? {
+ val pm = context.packageManager
+ var label: CharSequence? = null
+ // Try to load the label from the package's resources. If an app has not explicitly
+ // specified any label, just use the package name.
+ if (appInfo.labelRes != 0) {
+ try {
+ label = appInfo.loadLabel(pm)
+ } catch (e: Resources.NotFoundException) {
+ }
+ }
+ if (label == null) {
+ label = if (appInfo.nonLocalizedLabel != null) appInfo.nonLocalizedLabel
+ else appInfo.packageName
+ }
+ return label
+ }
+
+ /**
+ * Utility method to load application icon
+ *
+ * @param context context of package that can load the resources
+ * @param appInfo ApplicationInfo object of package whose resources are to be loaded
+ */
+ private fun getAppIconFromFile(context: Context, appInfo: ApplicationInfo): Drawable? {
+ val pm = context.packageManager
+ var icon: Drawable? = null
+ // Try to load the icon from the package's resources. If an app has not explicitly
+ // specified any resource, just use the default icon for now.
+ try {
+ if (appInfo.icon != 0) {
+ try {
+ icon = appInfo.loadIcon(pm)
+ } catch (e: Resources.NotFoundException) {
+ }
+ }
+ if (icon == null) {
+ icon = context.packageManager.defaultActivityIcon
+ }
+ } catch (e: OutOfMemoryError) {
+ Log.i(LOG_TAG, "Could not load app icon", e)
+ }
+ return icon
+ }
+
+ private fun processAppInfoForFile(appInfo: ApplicationInfo, sourceFile: File): ApplicationInfo {
+ val archiveFilePath = sourceFile.absolutePath
+ appInfo.publicSourceDir = archiveFilePath
+ if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
+ val files = sourceFile.parentFile?.listFiles()
+ val splits = appInfo.splitNames!!
+ .mapNotNull { findFilePath(files, "$it.apk") }
+ .toTypedArray()
+
+ appInfo.splitSourceDirs = splits
+ appInfo.splitPublicSourceDirs = splits
+ }
+ return appInfo
+ }
+
+ private fun findFilePath(files: Array<File>?, postfix: String): String? {
+ files?.let {
+ for (file in it) {
+ val path = file.absolutePath
+ if (path.endsWith(postfix)) {
+ return path
+ }
+ }
+ }
+ return null
+ }
+
+ /**
+ * @return the packageName corresponding to a UID.
+ */
+ @JvmStatic
+ fun getPackageNameForUid(context: Context, sourceUid: Int, callingPackage: String?): String? {
+ if (sourceUid == Process.INVALID_UID) {
+ return null
+ }
+ // If the sourceUid belongs to the system downloads provider, we explicitly return the
+ // name of the Download Manager package. This is because its UID is shared with multiple
+ // packages, resulting in uncertainty about which package will end up first in the list
+ // of packages associated with this UID
+ val pm = context.packageManager
+ val systemDownloadProviderInfo = getSystemDownloadsProviderInfo(pm, sourceUid)
+ if (systemDownloadProviderInfo != null) {
+ return systemDownloadProviderInfo.packageName
+ }
+ val packagesForUid = pm.getPackagesForUid(sourceUid) ?: return null
+ if (packagesForUid.size > 1) {
+ if (callingPackage != null) {
+ for (packageName in packagesForUid) {
+ if (packageName == callingPackage) {
+ return packageName
+ }
+ }
+ }
+ Log.i(LOG_TAG, "Multiple packages found for source uid $sourceUid")
+ }
+ return packagesForUid[0]
+ }
+
+ /**
+ * Utility method to get package information for a given [File]
+ */
+ @JvmStatic
+ fun getPackageInfo(context: Context, sourceFile: File, flags: Int): PackageInfo? {
+ var filePath = sourceFile.absolutePath
+ if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) {
+ val dir = sourceFile.parentFile
+ if ((dir?.listFiles()?.size ?: 0) > 1) {
+ // split apks, use file directory to get archive info
+ filePath = dir.path
+ }
+ }
+ return try {
+ context.packageManager.getPackageArchiveInfo(filePath, flags)
+ } catch (ignored: Exception) {
+ null
+ }
+ }
+
+ /**
+ * Is a profile part of a user?
+ *
+ * @param userManager The user manager
+ * @param userHandle The handle of the user
+ * @param profileHandle The handle of the profile
+ *
+ * @return If the profile is part of the user or the profile parent of the user
+ */
+ @JvmStatic
+ fun isProfileOfOrSame(
+ userManager: UserManager,
+ userHandle: UserHandle,
+ profileHandle: UserHandle?,
+ ): Boolean {
+ if (profileHandle == null) {
+ return false
+ }
+ return if (userHandle == profileHandle) {
+ true
+ } else userManager.getProfileParent(profileHandle) != null
+ && userManager.getProfileParent(profileHandle) == userHandle
+ }
+
+ /**
+ * The class to hold an incoming package's icon and label.
+ * See [getAppSnippet]
+ */
+ data class AppSnippet(var label: CharSequence?, var icon: Drawable?)
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
deleted file mode 100644
index a2c81f1..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model;
-
-import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH;
-
-import android.content.Context;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageInstaller.SessionInfo;
-import android.content.res.AssetFileDescriptor;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.util.Log;
-import androidx.lifecycle.MutableLiveData;
-import com.android.packageinstaller.v2.model.InstallRepository.SessionStageListener;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-public class SessionStager extends AsyncTask<Void, Integer, SessionInfo> {
-
- private static final String TAG = SessionStager.class.getSimpleName();
- private final Context mContext;
- private final Uri mUri;
- private final int mStagedSessionId;
- private final MutableLiveData<Integer> mProgressLiveData = new MutableLiveData<>(0);
- private final SessionStageListener mListener;
-
- SessionStager(Context context, Uri uri, int stagedSessionId, SessionStageListener listener) {
- mContext = context;
- mUri = uri;
- mStagedSessionId = stagedSessionId;
- mListener = listener;
- }
-
- @Override
- protected PackageInstaller.SessionInfo doInBackground(Void... params) {
- PackageInstaller pi = mContext.getPackageManager().getPackageInstaller();
- try (PackageInstaller.Session session = pi.openSession(mStagedSessionId);
- InputStream in = mContext.getContentResolver().openInputStream(mUri)) {
- session.setStagingProgress(0);
-
- if (in == null) {
- return null;
- }
- final long sizeBytes = getContentSizeBytes();
- mProgressLiveData.postValue(sizeBytes > 0 ? 0 : -1);
-
- long totalRead = 0;
- try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {
- byte[] buffer = new byte[1024 * 1024];
- while (true) {
- int numRead = in.read(buffer);
-
- if (numRead == -1) {
- session.fsync(out);
- break;
- }
-
- if (isCancelled()) {
- break;
- }
-
- out.write(buffer, 0, numRead);
- if (sizeBytes > 0) {
- totalRead += numRead;
- float fraction = ((float) totalRead / (float) sizeBytes);
- session.setStagingProgress(fraction);
- publishProgress((int) (fraction * 100.0));
- }
- }
- }
- return pi.getSessionInfo(mStagedSessionId);
- } catch (IOException | SecurityException | IllegalStateException
- | IllegalArgumentException e) {
- Log.w(TAG, "Error staging apk from content URI", e);
- return null;
- }
- }
-
- private long getContentSizeBytes() {
- try (AssetFileDescriptor afd = mContext.getContentResolver()
- .openAssetFileDescriptor(mUri, "r")) {
- return afd != null ? afd.getLength() : UNKNOWN_LENGTH;
- } catch (IOException e) {
- Log.w(TAG, "Failed to open asset file descriptor", e);
- return UNKNOWN_LENGTH;
- }
- }
-
- public MutableLiveData<Integer> getProgress() {
- return mProgressLiveData;
- }
-
- @Override
- protected void onProgressUpdate(Integer... progress) {
- if (progress != null && progress.length > 0) {
- mProgressLiveData.setValue(progress[0]);
- }
- }
-
- @Override
- protected void onPostExecute(SessionInfo sessionInfo) {
- if (sessionInfo == null || !sessionInfo.isActive()
- || sessionInfo.getResolvedBaseApkPath() == null) {
- Log.w(TAG, "Session info is invalid: " + sessionInfo);
- mListener.onStagingFailure();
- return;
- }
- mListener.onStagingSuccess(sessionInfo);
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt
new file mode 100644
index 0000000..c9bfa17
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/SessionStager.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.model
+
+import android.content.Context
+import android.content.pm.PackageInstaller
+import android.content.res.AssetFileDescriptor
+import android.net.Uri
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import java.io.IOException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+class SessionStager internal constructor(
+ private val context: Context,
+ private val uri: Uri,
+ private val stagedSessionId: Int
+) {
+
+ companion object {
+ private val LOG_TAG = SessionStager::class.java.simpleName
+ }
+
+ private val _progress = MutableLiveData(0)
+ val progress: LiveData<Int>
+ get() = _progress
+
+ suspend fun execute(): Boolean = withContext(Dispatchers.IO) {
+ val pi: PackageInstaller = context.packageManager.packageInstaller
+ var sessionInfo: PackageInstaller.SessionInfo?
+ try {
+ val session = pi.openSession(stagedSessionId)
+ context.contentResolver.openInputStream(uri).use { instream ->
+ session.setStagingProgress(0f)
+
+ if (instream == null) {
+ return@withContext false
+ }
+
+ val sizeBytes = getContentSizeBytes()
+ publishProgress(if (sizeBytes > 0) 0 else -1)
+
+ var totalRead: Long = 0
+ session.openWrite("PackageInstaller", 0, sizeBytes).use { out ->
+ val buffer = ByteArray(1024 * 1024)
+ while (true) {
+ val numRead = instream.read(buffer)
+ if (numRead == -1) {
+ session.fsync(out)
+ break
+ }
+ out.write(buffer, 0, numRead)
+
+ if (sizeBytes > 0) {
+ totalRead += numRead.toLong()
+ val fraction = totalRead.toFloat() / sizeBytes.toFloat()
+ session.setStagingProgress(fraction)
+ publishProgress((fraction * 100.0).toInt())
+ }
+ }
+ }
+ sessionInfo = pi.getSessionInfo(stagedSessionId)
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "Error staging apk from content URI", e)
+ sessionInfo = null
+ }
+
+ return@withContext if (sessionInfo == null
+ || !sessionInfo?.isActive!!
+ || sessionInfo?.resolvedBaseApkPath == null
+ ) {
+ Log.w(LOG_TAG, "Session info is invalid: $sessionInfo")
+ false
+ } else {
+ true
+ }
+ }
+
+ private fun getContentSizeBytes(): Long {
+ return try {
+ context.contentResolver
+ .openAssetFileDescriptor(uri, "r")
+ .use { afd -> afd?.length ?: AssetFileDescriptor.UNKNOWN_LENGTH }
+ } catch (e: IOException) {
+ Log.w(LOG_TAG, "Failed to open asset file descriptor", e)
+ AssetFileDescriptor.UNKNOWN_LENGTH
+ }
+ }
+
+ private suspend fun publishProgress(progressValue: Int) = withContext(Dispatchers.Main) {
+ _progress.value = progressValue
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
deleted file mode 100644
index a07c532..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
+++ /dev/null
@@ -1,716 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model;
-
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
-import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
-import static com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid;
-import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid;
-import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
-import static com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame;
-import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_APP_UNAVAILABLE;
-import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_GENERIC_ERROR;
-import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.AppOpsManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.admin.DevicePolicyManager;
-import android.app.usage.StorageStats;
-import android.app.usage.StorageStatsManager;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.UninstallCompleteCallback;
-import android.content.pm.VersionedPackage;
-import android.graphics.drawable.Icon;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Process;
-import android.os.UserHandle;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.util.Log;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.lifecycle.MutableLiveData;
-import com.android.packageinstaller.R;
-import com.android.packageinstaller.common.EventResultPersister;
-import com.android.packageinstaller.common.UninstallEventReceiver;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallReady;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
-import java.io.IOException;
-import java.util.List;
-
-public class UninstallRepository {
-
- private static final String TAG = UninstallRepository.class.getSimpleName();
- private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall_failure";
- private static final String BROADCAST_ACTION =
- "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
-
- private static final String EXTRA_UNINSTALL_ID =
- "com.android.packageinstaller.extra.UNINSTALL_ID";
- private static final String EXTRA_APP_LABEL =
- "com.android.packageinstaller.extra.APP_LABEL";
- private static final String EXTRA_IS_CLONE_APP =
- "com.android.packageinstaller.extra.IS_CLONE_APP";
- private static final String EXTRA_PACKAGE_NAME =
- "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME";
-
- private final Context mContext;
- private final AppOpsManager mAppOpsManager;
- private final PackageManager mPackageManager;
- private final UserManager mUserManager;
- private final NotificationManager mNotificationManager;
- private final MutableLiveData<UninstallStage> mUninstallResult = new MutableLiveData<>();
- public UserHandle mUninstalledUser;
- public UninstallCompleteCallback mCallback;
- private ApplicationInfo mTargetAppInfo;
- private ActivityInfo mTargetActivityInfo;
- private Intent mIntent;
- private CharSequence mTargetAppLabel;
- private String mTargetPackageName;
- private String mCallingActivity;
- private boolean mUninstallFromAllUsers;
- private boolean mIsClonedApp;
- private int mUninstallId;
-
- public UninstallRepository(Context context) {
- mContext = context;
- mAppOpsManager = context.getSystemService(AppOpsManager.class);
- mPackageManager = context.getPackageManager();
- mUserManager = context.getSystemService(UserManager.class);
- mNotificationManager = context.getSystemService(NotificationManager.class);
- }
-
- public UninstallStage performPreUninstallChecks(Intent intent, CallerInfo callerInfo) {
- mIntent = intent;
-
- int callingUid = callerInfo.getUid();
- mCallingActivity = callerInfo.getActivityName();
-
- if (callingUid == Process.INVALID_UID) {
- Log.e(TAG, "Could not determine the launching uid.");
- return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
- // TODO: should we give any indication to the user?
- }
-
- String callingPackage = getPackageNameForUid(mContext, callingUid, null);
- if (callingPackage == null) {
- Log.e(TAG, "Package not found for originating uid " + callingUid);
- return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
- } else {
- if (mAppOpsManager.noteOpNoThrow(
- AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage)
- != MODE_ALLOWED) {
- Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps");
- return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
- }
- }
-
- if (getMaxTargetSdkVersionForUid(mContext, callingUid) >= Build.VERSION_CODES.P
- && !isPermissionGranted(mContext, Manifest.permission.REQUEST_DELETE_PACKAGES,
- callingUid)
- && !isPermissionGranted(mContext, Manifest.permission.DELETE_PACKAGES, callingUid)) {
- Log.e(TAG, "Uid " + callingUid + " does not have "
- + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
- + Manifest.permission.DELETE_PACKAGES);
-
- return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
- }
-
- // Get intent information.
- // We expect an intent with URI of the form package:<packageName>#<className>
- // className is optional; if specified, it is the activity the user chose to uninstall
- final Uri packageUri = intent.getData();
- if (packageUri == null) {
- Log.e(TAG, "No package URI in intent");
- return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
- }
- mTargetPackageName = packageUri.getEncodedSchemeSpecificPart();
- if (mTargetPackageName == null) {
- Log.e(TAG, "Invalid package name in URI: " + packageUri);
- return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
- }
-
- mUninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS,
- false);
- if (mUninstallFromAllUsers && !mUserManager.isAdminUser()) {
- Log.e(TAG, "Only admin user can request uninstall for all users");
- return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
- }
-
- mUninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
- if (mUninstalledUser == null) {
- mUninstalledUser = Process.myUserHandle();
- } else {
- List<UserHandle> profiles = mUserManager.getUserProfiles();
- if (!profiles.contains(mUninstalledUser)) {
- Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall "
- + "for user " + mUninstalledUser);
- return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
- }
- }
-
- mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
- PackageManager.UninstallCompleteCallback.class);
-
- try {
- mTargetAppInfo = mPackageManager.getApplicationInfo(mTargetPackageName,
- PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER));
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to get packageName");
- }
-
- if (mTargetAppInfo == null) {
- Log.e(TAG, "Invalid packageName: " + mTargetPackageName);
- return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
- }
-
- // The class name may have been specified (e.g. when deleting an app from all apps)
- final String className = packageUri.getFragment();
- if (className != null) {
- try {
- mTargetActivityInfo = mPackageManager.getActivityInfo(
- new ComponentName(mTargetPackageName, className),
- PackageManager.ComponentInfoFlags.of(0));
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Unable to get className");
- // Continue as the ActivityInfo isn't critical.
- }
- }
-
- return new UninstallReady();
- }
-
- public UninstallStage generateUninstallDetails() {
- UninstallUserActionRequired.Builder uarBuilder = new UninstallUserActionRequired.Builder();
- StringBuilder messageBuilder = new StringBuilder();
-
- mTargetAppLabel = mTargetAppInfo.loadSafeLabel(mPackageManager);
-
- // If the Activity label differs from the App label, then make sure the user
- // knows the Activity belongs to the App being uninstalled.
- if (mTargetActivityInfo != null) {
- final CharSequence activityLabel = mTargetActivityInfo.loadSafeLabel(mPackageManager);
- if (CharSequence.compare(activityLabel, mTargetAppLabel) != 0) {
- messageBuilder.append(
- mContext.getString(R.string.uninstall_activity_text, activityLabel));
- messageBuilder.append(" ").append(mTargetAppLabel).append(".\n\n");
- }
- }
-
- final boolean isUpdate =
- (mTargetAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
- final UserHandle myUserHandle = Process.myUserHandle();
- boolean isSingleUser = isSingleUser();
-
- if (isUpdate) {
- messageBuilder.append(mContext.getString(
- isSingleUser ? R.string.uninstall_update_text :
- R.string.uninstall_update_text_multiuser));
- } else if (mUninstallFromAllUsers && !isSingleUser) {
- messageBuilder.append(mContext.getString(
- R.string.uninstall_application_text_all_users));
- } else if (!mUninstalledUser.equals(myUserHandle)) {
- // Uninstalling user is issuing uninstall for another user
- UserManager customUserManager = mContext.createContextAsUser(mUninstalledUser, 0)
- .getSystemService(UserManager.class);
- String userName = customUserManager.getUserName();
-
- String uninstalledUserType = getUninstalledUserType(myUserHandle, mUninstalledUser);
- String messageString;
- if (USER_TYPE_PROFILE_MANAGED.equals(uninstalledUserType)) {
- messageString = mContext.getString(
- R.string.uninstall_application_text_current_user_work_profile, userName);
- } else if (USER_TYPE_PROFILE_CLONE.equals(uninstalledUserType)) {
- mIsClonedApp = true;
- messageString = mContext.getString(
- R.string.uninstall_application_text_current_user_clone_profile);
- } else {
- messageString = mContext.getString(
- R.string.uninstall_application_text_user, userName);
- }
- messageBuilder.append(messageString);
- } else if (isCloneProfile(mUninstalledUser)) {
- mIsClonedApp = true;
- messageBuilder.append(mContext.getString(
- R.string.uninstall_application_text_current_user_clone_profile));
- } else if (myUserHandle.equals(UserHandle.SYSTEM)
- && hasClonedInstance(mTargetAppInfo.packageName)) {
- messageBuilder.append(mContext.getString(
- R.string.uninstall_application_text_with_clone_instance, mTargetAppLabel));
- } else {
- messageBuilder.append(mContext.getString(R.string.uninstall_application_text));
- }
-
- uarBuilder.setMessage(messageBuilder.toString());
-
- if (mIsClonedApp) {
- uarBuilder.setTitle(mContext.getString(R.string.cloned_app_label, mTargetAppLabel));
- } else {
- uarBuilder.setTitle(mTargetAppLabel.toString());
- }
-
- boolean suggestToKeepAppData = false;
- try {
- PackageInfo pkgInfo = mPackageManager.getPackageInfo(mTargetPackageName, 0);
- suggestToKeepAppData =
- pkgInfo.applicationInfo != null && pkgInfo.applicationInfo.hasFragileUserData();
- } catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "Cannot check hasFragileUserData for " + mTargetPackageName, e);
- }
-
- long appDataSize = 0;
- if (suggestToKeepAppData) {
- appDataSize = getAppDataSize(mTargetPackageName,
- mUninstallFromAllUsers ? null : mUninstalledUser);
- }
- uarBuilder.setAppDataSize(appDataSize);
-
- return uarBuilder.build();
- }
-
- /**
- * Returns whether there is only one "full" user on this device.
- *
- * <p><b>Note:</b> on devices that use {@link android.os.UserManager#isHeadlessSystemUserMode()
- * headless system user mode}, the system user is not "full", so it's not be considered in the
- * calculation.</p>
- */
- private boolean isSingleUser() {
- final int userCount = mUserManager.getUserCount();
- return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2);
- }
-
- /**
- * Returns the type of the user from where an app is being uninstalled. We are concerned with
- * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
- * belong to the same profile group.
- */
- @Nullable
- private String getUninstalledUserType(UserHandle myUserHandle,
- UserHandle uninstalledUserHandle) {
- if (!mUserManager.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
- return null;
- }
-
- UserManager customUserManager = mContext.createContextAsUser(uninstalledUserHandle, 0)
- .getSystemService(UserManager.class);
- String[] userTypes = {USER_TYPE_PROFILE_MANAGED, USER_TYPE_PROFILE_CLONE};
- for (String userType : userTypes) {
- if (customUserManager.isUserOfType(userType)) {
- return userType;
- }
- }
- return null;
- }
-
- private boolean hasClonedInstance(String packageName) {
- // Check if clone user is present on the device.
- UserHandle cloneUser = null;
- List<UserHandle> profiles = mUserManager.getUserProfiles();
- for (UserHandle userHandle : profiles) {
- if (!userHandle.equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) {
- cloneUser = userHandle;
- break;
- }
- }
- // Check if another instance of given package exists in clone user profile.
- try {
- return cloneUser != null
- && mPackageManager.getPackageUidAsUser(packageName,
- PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
- }
-
- private boolean isCloneProfile(UserHandle userHandle) {
- UserManager customUserManager = mContext.createContextAsUser(userHandle, 0)
- .getSystemService(UserManager.class);
- return customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE);
- }
-
- /**
- * Get number of bytes of the app data of the package.
- *
- * @param pkg The package that might have app data.
- * @param user The user the package belongs to or {@code null} if files of all users should
- * be counted.
- * @return The number of bytes.
- */
- private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) {
- if (user != null) {
- return getAppDataSizeForUser(pkg, user);
- }
- // We are uninstalling from all users. Get cumulative app data size for all users.
- List<UserHandle> userHandles = mUserManager.getUserHandles(true);
- long totalAppDataSize = 0;
- int numUsers = userHandles.size();
- for (int i = 0; i < numUsers; i++) {
- totalAppDataSize += getAppDataSizeForUser(pkg, userHandles.get(i));
- }
- return totalAppDataSize;
- }
-
- /**
- * Get number of bytes of the app data of the package.
- *
- * @param pkg The package that might have app data.
- * @param user The user the package belongs to
- * @return The number of bytes.
- */
- private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) {
- StorageStatsManager storageStatsManager =
- mContext.getSystemService(StorageStatsManager.class);
- try {
- StorageStats stats = storageStatsManager.queryStatsForPackage(
- mPackageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user);
- return stats.getDataBytes();
- } catch (PackageManager.NameNotFoundException | IOException | SecurityException e) {
- Log.e(TAG, "Cannot determine amount of app data for " + pkg, e);
- }
- return 0;
- }
-
- public void initiateUninstall(boolean keepData) {
- // Get an uninstallId to track results and show a notification on non-TV devices.
- try {
- mUninstallId = UninstallEventReceiver.addObserver(mContext,
- EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult);
- } catch (EventResultPersister.OutOfIdsException e) {
- Log.e(TAG, "Failed to start uninstall", e);
- handleUninstallResult(PackageInstaller.STATUS_FAILURE,
- PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
- return;
- }
-
- // TODO: Check with UX whether to show UninstallUninstalling dialog / notification?
- mUninstallResult.setValue(new UninstallUninstalling(mTargetAppLabel, mIsClonedApp));
-
- Bundle uninstallData = new Bundle();
- uninstallData.putInt(EXTRA_UNINSTALL_ID, mUninstallId);
- uninstallData.putString(EXTRA_PACKAGE_NAME, mTargetPackageName);
- uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, mUninstallFromAllUsers);
- uninstallData.putCharSequence(EXTRA_APP_LABEL, mTargetAppLabel);
- uninstallData.putBoolean(EXTRA_IS_CLONE_APP, mIsClonedApp);
- Log.i(TAG, "Uninstalling extras = " + uninstallData);
-
- // Get a PendingIntent for result broadcast and issue an uninstall request
- Intent broadcastIntent = new Intent(BROADCAST_ACTION);
- broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
- broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mUninstallId);
- broadcastIntent.setPackage(mContext.getPackageName());
-
- PendingIntent pendingIntent =
- PendingIntent.getBroadcast(mContext, mUninstallId, broadcastIntent,
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
-
- if (!startUninstall(mTargetPackageName, mUninstalledUser, pendingIntent,
- mUninstallFromAllUsers, keepData)) {
- handleUninstallResult(PackageInstaller.STATUS_FAILURE,
- PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
- }
- }
-
- private void handleUninstallResult(int status, int legacyStatus, @Nullable String message,
- int serviceId) {
- if (mCallback != null) {
- // The caller will be informed about the result via a callback
- mCallback.onUninstallComplete(mTargetPackageName, legacyStatus, message);
-
- // Since the caller already received the results, just finish the app at this point
- mUninstallResult.setValue(null);
- return;
- }
-
- boolean returnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
- if (returnResult || mCallingActivity != null) {
- Intent intent = new Intent();
- intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus);
-
- if (status == PackageInstaller.STATUS_SUCCESS) {
- UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
- .setResultIntent(intent)
- .setActivityResultCode(Activity.RESULT_OK);
- mUninstallResult.setValue(successBuilder.build());
- } else {
- UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(true)
- .setResultIntent(intent)
- .setActivityResultCode(Activity.RESULT_FIRST_USER);
- mUninstallResult.setValue(failedBuilder.build());
- }
- return;
- }
-
- // Caller did not want the result back. So, we either show a Toast, or a Notification.
- if (status == PackageInstaller.STATUS_SUCCESS) {
- UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
- .setActivityResultCode(legacyStatus)
- .setMessage(mIsClonedApp
- ? mContext.getString(R.string.uninstall_done_clone_app, mTargetAppLabel)
- : mContext.getString(R.string.uninstall_done_app, mTargetAppLabel));
- mUninstallResult.setValue(successBuilder.build());
- } else {
- UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(false);
- Notification.Builder uninstallFailedNotification = null;
-
- NotificationChannel uninstallFailureChannel = new NotificationChannel(
- UNINSTALL_FAILURE_CHANNEL,
- mContext.getString(R.string.uninstall_failure_notification_channel),
- NotificationManager.IMPORTANCE_DEFAULT);
- mNotificationManager.createNotificationChannel(uninstallFailureChannel);
-
- uninstallFailedNotification = new Notification.Builder(mContext,
- UNINSTALL_FAILURE_CHANNEL);
-
- UserHandle myUserHandle = Process.myUserHandle();
- switch (legacyStatus) {
- case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> {
- // Find out if the package is an active admin for some non-current user.
- UserHandle otherBlockingUserHandle =
- findUserOfDeviceAdmin(myUserHandle, mTargetPackageName);
-
- if (otherBlockingUserHandle == null) {
- Log.d(TAG, "Uninstall failed because " + mTargetPackageName
- + " is a device admin");
-
- addDeviceManagerButton(mContext, uninstallFailedNotification);
- setBigText(uninstallFailedNotification, mContext.getString(
- R.string.uninstall_failed_device_policy_manager));
- } else {
- Log.d(TAG, "Uninstall failed because " + mTargetPackageName
- + " is a device admin of user " + otherBlockingUserHandle);
-
- String userName =
- mContext.createContextAsUser(otherBlockingUserHandle, 0)
- .getSystemService(UserManager.class).getUserName();
- setBigText(uninstallFailedNotification, String.format(
- mContext.getString(
- R.string.uninstall_failed_device_policy_manager_of_user),
- userName));
- }
- }
- case PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
- UserHandle otherBlockingUserHandle = findBlockingUser(mTargetPackageName);
- boolean isProfileOfOrSame = isProfileOfOrSame(mUserManager, myUserHandle,
- otherBlockingUserHandle);
-
- if (isProfileOfOrSame) {
- addDeviceManagerButton(mContext, uninstallFailedNotification);
- } else {
- addManageUsersButton(mContext, uninstallFailedNotification);
- }
-
- String bigText = null;
- if (otherBlockingUserHandle == null) {
- Log.d(TAG, "Uninstall failed for " + mTargetPackageName +
- " with code " + status + " no blocking user");
- } else if (otherBlockingUserHandle == UserHandle.SYSTEM) {
- bigText = mContext.getString(
- R.string.uninstall_blocked_device_owner);
- } else {
- bigText = mContext.getString(mUninstallFromAllUsers ?
- R.string.uninstall_all_blocked_profile_owner
- : R.string.uninstall_blocked_profile_owner);
- }
- if (bigText != null) {
- setBigText(uninstallFailedNotification, bigText);
- }
- }
- default -> {
- Log.d(TAG, "Uninstall blocked for " + mTargetPackageName
- + " with legacy code " + legacyStatus);
- }
- }
-
- uninstallFailedNotification.setContentTitle(
- mContext.getString(R.string.uninstall_failed_app, mTargetAppLabel));
- uninstallFailedNotification.setOngoing(false);
- uninstallFailedNotification.setSmallIcon(R.drawable.ic_error);
- failedBuilder.setUninstallNotification(mUninstallId,
- uninstallFailedNotification.build());
-
- mUninstallResult.setValue(failedBuilder.build());
- }
- }
-
- /**
- * @param myUserHandle {@link UserHandle} of the current user.
- * @param packageName Name of the package being uninstalled.
- * @return the {@link UserHandle} of the user in which a package is a device admin.
- */
- @Nullable
- private UserHandle findUserOfDeviceAdmin(UserHandle myUserHandle, String packageName) {
- for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
- // We only catch the case when the user in question is neither the
- // current user nor its profile.
- if (isProfileOfOrSame(mUserManager, myUserHandle, otherUserHandle)) {
- continue;
- }
- DevicePolicyManager dpm = mContext.createContextAsUser(otherUserHandle, 0)
- .getSystemService(DevicePolicyManager.class);
- if (dpm.packageHasActiveAdmins(packageName)) {
- return otherUserHandle;
- }
- }
- return null;
- }
-
- /**
- *
- * @param packageName Name of the package being uninstalled.
- * @return {@link UserHandle} of the user in which a package is blocked from being uninstalled.
- */
- @Nullable
- private UserHandle findBlockingUser(String packageName) {
- for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
- // TODO (b/307399586): Add a negation when the logic of the method
- // is fixed
- if (mPackageManager.canUserUninstall(packageName, otherUserHandle)) {
- return otherUserHandle;
- }
- }
- return null;
- }
-
- /**
- * Set big text for the notification.
- *
- * @param builder The builder of the notification
- * @param text The text to set.
- */
- private void setBigText(@NonNull Notification.Builder builder,
- @NonNull CharSequence text) {
- builder.setStyle(new Notification.BigTextStyle().bigText(text));
- }
-
- /**
- * Add a button to the notification that links to the user management.
- *
- * @param context The context the notification is created in
- * @param builder The builder of the notification
- */
- private void addManageUsersButton(@NonNull Context context,
- @NonNull Notification.Builder builder) {
- builder.addAction((new Notification.Action.Builder(
- Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
- context.getString(R.string.manage_users),
- PendingIntent.getActivity(context, 0, getUserSettingsIntent(),
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
- }
-
- private Intent getUserSettingsIntent() {
- Intent intent = new Intent(Settings.ACTION_USER_SETTINGS);
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
- return intent;
- }
-
- /**
- * Add a button to the notification that links to the device policy management.
- *
- * @param context The context the notification is created in
- * @param builder The builder of the notification
- */
- private void addDeviceManagerButton(@NonNull Context context,
- @NonNull Notification.Builder builder) {
- builder.addAction((new Notification.Action.Builder(
- Icon.createWithResource(context, R.drawable.ic_lock),
- context.getString(R.string.manage_device_administrators),
- PendingIntent.getActivity(context, 0, getDeviceManagerIntent(),
- PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
- }
-
- private Intent getDeviceManagerIntent() {
- Intent intent = new Intent();
- intent.setClassName("com.android.settings",
- "com.android.settings.Settings$DeviceAdminSettingsActivity");
- intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
- return intent;
- }
-
- /**
- * Starts an uninstall for the given package.
- *
- * @return {@code true} if there was no exception while uninstalling. This does not represent
- * the result of the uninstall. Result will be made available in
- * {@link #handleUninstallResult(int, int, String, int)}
- */
- private boolean startUninstall(String packageName, UserHandle targetUser,
- PendingIntent pendingIntent, boolean uninstallFromAllUsers, boolean keepData) {
- int flags = uninstallFromAllUsers ? PackageManager.DELETE_ALL_USERS : 0;
- flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
- try {
- mContext.createContextAsUser(targetUser, 0)
- .getPackageManager().getPackageInstaller().uninstall(
- new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
- flags, pendingIntent.getIntentSender());
- return true;
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Failed to uninstall", e);
- return false;
- }
- }
-
- public void cancelInstall() {
- if (mCallback != null) {
- mCallback.onUninstallComplete(mTargetPackageName,
- PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user");
- }
- }
-
- public MutableLiveData<UninstallStage> getUninstallResult() {
- return mUninstallResult;
- }
-
- public static class CallerInfo {
-
- private final String mActivityName;
- private final int mUid;
-
- public CallerInfo(String activityName, int uid) {
- mActivityName = activityName;
- mUid = uid;
- }
-
- public String getActivityName() {
- return mActivityName;
- }
-
- public int getUid() {
- return mUid;
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
new file mode 100644
index 0000000..7cc95c5
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
@@ -0,0 +1,739 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ * https://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.packageinstaller.v2.model
+
+import android.Manifest
+import android.app.Activity
+import android.app.AppOpsManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.admin.DevicePolicyManager
+import android.app.usage.StorageStatsManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.content.pm.VersionedPackage
+import android.graphics.drawable.Icon
+import android.os.Build
+import android.os.Bundle
+import android.os.Process
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import android.util.Log
+import androidx.lifecycle.MutableLiveData
+import com.android.packageinstaller.R
+import com.android.packageinstaller.common.EventResultPersister
+import com.android.packageinstaller.common.EventResultPersister.OutOfIdsException
+import com.android.packageinstaller.common.UninstallEventReceiver
+import com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid
+import com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid
+import com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted
+import com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame
+
+class UninstallRepository(private val context: Context) {
+
+ private val appOpsManager: AppOpsManager? = context.getSystemService(AppOpsManager::class.java)
+ private val packageManager: PackageManager = context.packageManager
+ private val userManager: UserManager? = context.getSystemService(UserManager::class.java)
+ private val notificationManager: NotificationManager? =
+ context.getSystemService(NotificationManager::class.java)
+ val uninstallResult = MutableLiveData<UninstallStage?>()
+ private var uninstalledUser: UserHandle? = null
+ private var callback: PackageManager.UninstallCompleteCallback? = null
+ private var targetAppInfo: ApplicationInfo? = null
+ private var targetActivityInfo: ActivityInfo? = null
+ private lateinit var intent: Intent
+ private lateinit var targetAppLabel: CharSequence
+ private var targetPackageName: String? = null
+ private var callingActivity: String? = null
+ private var uninstallFromAllUsers = false
+ private var isClonedApp = false
+ private var uninstallId = 0
+
+ fun performPreUninstallChecks(intent: Intent, callerInfo: CallerInfo): UninstallStage {
+ this.intent = intent
+
+ val callingUid = callerInfo.uid
+ callingActivity = callerInfo.activityName
+
+ if (callingUid == Process.INVALID_UID) {
+ Log.e(LOG_TAG, "Could not determine the launching uid.")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
+ // TODO: should we give any indication to the user?
+ }
+
+ val callingPackage = getPackageNameForUid(context, callingUid, null)
+ if (callingPackage == null) {
+ Log.e(LOG_TAG, "Package not found for originating uid $callingUid")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
+ } else {
+ if (appOpsManager!!.noteOpNoThrow(
+ AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage
+ ) != AppOpsManager.MODE_ALLOWED
+ ) {
+ Log.e(LOG_TAG, "Install from uid $callingUid disallowed by AppOps")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
+ }
+ }
+
+ if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P
+ && !isPermissionGranted(
+ context, Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid
+ )
+ && !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid)
+ ) {
+ Log.e(
+ LOG_TAG, "Uid " + callingUid + " does not have "
+ + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
+ + Manifest.permission.DELETE_PACKAGES
+ )
+ return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
+ }
+
+ // Get intent information.
+ // We expect an intent with URI of the form package:<packageName>#<className>
+ // className is optional; if specified, it is the activity the user chose to uninstall
+ val packageUri = intent.data
+ if (packageUri == null) {
+ Log.e(LOG_TAG, "No package URI in intent")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE)
+ }
+ targetPackageName = packageUri.encodedSchemeSpecificPart
+ if (targetPackageName == null) {
+ Log.e(LOG_TAG, "Invalid package name in URI: $packageUri")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE)
+ }
+
+ uninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false)
+ if (uninstallFromAllUsers && !userManager!!.isAdminUser) {
+ Log.e(LOG_TAG, "Only admin user can request uninstall for all users")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED)
+ }
+
+ uninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle::class.java)
+ if (uninstalledUser == null) {
+ uninstalledUser = Process.myUserHandle()
+ } else {
+ val profiles = userManager!!.userProfiles
+ if (!profiles.contains(uninstalledUser)) {
+ Log.e(
+ LOG_TAG, "User " + Process.myUserHandle() + " can't request uninstall "
+ + "for user " + uninstalledUser
+ )
+ return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED)
+ }
+ }
+
+ callback = intent.getParcelableExtra(
+ PackageInstaller.EXTRA_CALLBACK, PackageManager.UninstallCompleteCallback::class.java
+ )
+
+ try {
+ targetAppInfo = packageManager.getApplicationInfo(
+ targetPackageName!!,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER.toLong())
+ )
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(LOG_TAG, "Unable to get packageName")
+ }
+
+ if (targetAppInfo == null) {
+ Log.e(LOG_TAG, "Invalid packageName: $targetPackageName")
+ return UninstallAborted(UninstallAborted.ABORT_REASON_APP_UNAVAILABLE)
+ }
+
+ // The class name may have been specified (e.g. when deleting an app from all apps)
+ val className = packageUri.fragment
+ if (className != null) {
+ try {
+ targetActivityInfo = packageManager.getActivityInfo(
+ ComponentName(targetPackageName!!, className),
+ PackageManager.ComponentInfoFlags.of(0)
+ )
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(LOG_TAG, "Unable to get className")
+ // Continue as the ActivityInfo isn't critical.
+ }
+ }
+
+ return UninstallReady()
+ }
+
+ fun generateUninstallDetails(): UninstallStage {
+ val messageBuilder = StringBuilder()
+
+ targetAppLabel = targetAppInfo!!.loadSafeLabel(packageManager)
+
+ // If the Activity label differs from the App label, then make sure the user
+ // knows the Activity belongs to the App being uninstalled.
+ if (targetActivityInfo != null) {
+ val activityLabel = targetActivityInfo!!.loadSafeLabel(packageManager)
+ if (!activityLabel.contentEquals(targetAppLabel)) {
+ messageBuilder.append(
+ context.getString(R.string.uninstall_activity_text, activityLabel)
+ )
+ messageBuilder.append(" ").append(targetAppLabel).append(".\n\n")
+ }
+ }
+
+ val isUpdate = (targetAppInfo!!.flags and ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
+ val myUserHandle = Process.myUserHandle()
+ val isSingleUser = isSingleUser()
+
+ if (isUpdate) {
+ messageBuilder.append(context.getString(
+ if (isSingleUser) R.string.uninstall_update_text
+ else R.string.uninstall_update_text_multiuser
+ )
+ )
+ } else if (uninstallFromAllUsers && !isSingleUser) {
+ messageBuilder.append(context.getString(R.string.uninstall_application_text_all_users))
+ } else if (uninstalledUser != myUserHandle) {
+ // Uninstalling user is issuing uninstall for another user
+ val customUserManager = context.createContextAsUser(uninstalledUser!!, 0)
+ .getSystemService(UserManager::class.java)
+ val userName = customUserManager!!.userName
+
+ val uninstalledUserType = getUninstalledUserType(myUserHandle, uninstalledUser!!)
+ val messageString: String
+ when (uninstalledUserType) {
+ UserManager.USER_TYPE_PROFILE_MANAGED -> {
+ messageString = context.getString(
+ R.string.uninstall_application_text_current_user_work_profile, userName
+ )
+ }
+
+ UserManager.USER_TYPE_PROFILE_CLONE -> {
+ isClonedApp = true
+ messageString = context.getString(
+ R.string.uninstall_application_text_current_user_clone_profile
+ )
+ }
+
+ else -> {
+ messageString = context.getString(
+ R.string.uninstall_application_text_user, userName
+ )
+ }
+
+ }
+ messageBuilder.append(messageString)
+ } else if (isCloneProfile(uninstalledUser!!)) {
+ isClonedApp = true
+ messageBuilder.append(context.getString(
+ R.string.uninstall_application_text_current_user_clone_profile
+ )
+ )
+ } else if (myUserHandle == UserHandle.SYSTEM
+ && hasClonedInstance(targetAppInfo!!.packageName)
+ ) {
+ messageBuilder.append(context.getString(
+ R.string.uninstall_application_text_with_clone_instance, targetAppLabel
+ )
+ )
+ } else {
+ messageBuilder.append(context.getString(R.string.uninstall_application_text))
+ }
+
+ val message = messageBuilder.toString()
+
+ val title = if (isClonedApp) {
+ context.getString(R.string.cloned_app_label, targetAppLabel)
+ } else {
+ targetAppLabel.toString()
+ }
+
+ var suggestToKeepAppData = false
+ try {
+ val pkgInfo = packageManager.getPackageInfo(targetPackageName!!, 0)
+ suggestToKeepAppData =
+ pkgInfo.applicationInfo != null && pkgInfo.applicationInfo!!.hasFragileUserData()
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(LOG_TAG, "Cannot check hasFragileUserData for $targetPackageName", e)
+ }
+
+ var appDataSize: Long = 0
+ if (suggestToKeepAppData) {
+ appDataSize = getAppDataSize(
+ targetPackageName!!,
+ if (uninstallFromAllUsers) null else uninstalledUser
+ )
+ }
+
+ return UninstallUserActionRequired(title, message, appDataSize)
+ }
+
+ /**
+ * Returns whether there is only one "full" user on this device.
+ *
+ * **Note:** On devices that use [headless system user mode]
+ * [android.os.UserManager.isHeadlessSystemUserMode], the system user is not "full",
+ * so it's not be considered in the calculation.
+ */
+ private fun isSingleUser(): Boolean {
+ val userCount = userManager!!.userCount
+ return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2)
+ }
+
+ /**
+ * Returns the type of the user from where an app is being uninstalled. We are concerned with
+ * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
+ * belong to the same profile group.
+ */
+ private fun getUninstalledUserType(
+ myUserHandle: UserHandle,
+ uninstalledUserHandle: UserHandle
+ ): String? {
+ if (!userManager!!.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
+ return null
+ }
+ val customUserManager = context.createContextAsUser(uninstalledUserHandle, 0)
+ .getSystemService(UserManager::class.java)
+ val userTypes =
+ arrayOf(UserManager.USER_TYPE_PROFILE_MANAGED, UserManager.USER_TYPE_PROFILE_CLONE)
+
+ for (userType in userTypes) {
+ if (customUserManager!!.isUserOfType(userType)) {
+ return userType
+ }
+ }
+ return null
+ }
+
+ private fun hasClonedInstance(packageName: String): Boolean {
+ // Check if clone user is present on the device.
+ var cloneUser: UserHandle? = null
+ val profiles = userManager!!.userProfiles
+
+ for (userHandle in profiles) {
+ if (userHandle != UserHandle.SYSTEM && isCloneProfile(userHandle)) {
+ cloneUser = userHandle
+ break
+ }
+ }
+ // Check if another instance of given package exists in clone user profile.
+ return try {
+ cloneUser != null
+ && packageManager.getPackageUidAsUser(
+ packageName, PackageManager.PackageInfoFlags.of(0), cloneUser.identifier
+ ) > 0
+ } catch (e: PackageManager.NameNotFoundException) {
+ false
+ }
+ }
+
+ private fun isCloneProfile(userHandle: UserHandle): Boolean {
+ val customUserManager = context.createContextAsUser(userHandle, 0)
+ .getSystemService(UserManager::class.java)
+ return customUserManager!!.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to or `null` if files of all users should
+ * be counted.
+ * @return The number of bytes.
+ */
+ private fun getAppDataSize(pkg: String, user: UserHandle?): Long {
+ if (user != null) {
+ return getAppDataSizeForUser(pkg, user)
+ }
+ // We are uninstalling from all users. Get cumulative app data size for all users.
+ val userHandles = userManager!!.getUserHandles(true)
+ var totalAppDataSize: Long = 0
+ val numUsers = userHandles.size
+ for (i in 0 until numUsers) {
+ totalAppDataSize += getAppDataSizeForUser(pkg, userHandles[i])
+ }
+ return totalAppDataSize
+ }
+
+ /**
+ * Get number of bytes of the app data of the package.
+ *
+ * @param pkg The package that might have app data.
+ * @param user The user the package belongs to
+ * @return The number of bytes.
+ */
+ private fun getAppDataSizeForUser(pkg: String, user: UserHandle): Long {
+ val storageStatsManager = context.getSystemService(StorageStatsManager::class.java)
+ try {
+ val stats = storageStatsManager!!.queryStatsForPackage(
+ packageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user
+ )
+ return stats.getDataBytes()
+ } catch (e: Exception) {
+ Log.e(LOG_TAG, "Cannot determine amount of app data for $pkg", e)
+ }
+ return 0
+ }
+
+ fun initiateUninstall(keepData: Boolean) {
+ // Get an uninstallId to track results and show a notification on non-TV devices.
+ uninstallId = try {
+ UninstallEventReceiver.addObserver(
+ context, EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult
+ )
+ } catch (e: OutOfIdsException) {
+ Log.e(LOG_TAG, "Failed to start uninstall", e)
+ handleUninstallResult(
+ PackageInstaller.STATUS_FAILURE,
+ PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0
+ )
+ return
+ }
+
+ // TODO: Check with UX whether to show UninstallUninstalling dialog / notification?
+ uninstallResult.value = UninstallUninstalling(targetAppLabel, isClonedApp)
+
+ val uninstallData = Bundle()
+ uninstallData.putInt(EXTRA_UNINSTALL_ID, uninstallId)
+ uninstallData.putString(EXTRA_PACKAGE_NAME, targetPackageName)
+ uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, uninstallFromAllUsers)
+ uninstallData.putCharSequence(EXTRA_APP_LABEL, targetAppLabel)
+ uninstallData.putBoolean(EXTRA_IS_CLONE_APP, isClonedApp)
+ Log.i(LOG_TAG, "Uninstalling extras = $uninstallData")
+
+ // Get a PendingIntent for result broadcast and issue an uninstall request
+ val broadcastIntent = Intent(BROADCAST_ACTION)
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId)
+ broadcastIntent.setPackage(context.packageName)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context, uninstallId, broadcastIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ )
+ if (!startUninstall(
+ targetPackageName!!, uninstalledUser!!, pendingIntent, uninstallFromAllUsers,
+ keepData
+ )
+ ) {
+ handleUninstallResult(
+ PackageInstaller.STATUS_FAILURE,
+ PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0
+ )
+ }
+ }
+
+ private fun handleUninstallResult(
+ status: Int,
+ legacyStatus: Int,
+ message: String?,
+ serviceId: Int
+ ) {
+ if (callback != null) {
+ // The caller will be informed about the result via a callback
+ callback!!.onUninstallComplete(targetPackageName!!, legacyStatus, message)
+
+ // Since the caller already received the results, just finish the app at this point
+ uninstallResult.value = null
+ return
+ }
+ val returnResult = intent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)
+ if (returnResult || callingActivity != null) {
+ val intent = Intent()
+ intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus)
+ if (status == PackageInstaller.STATUS_SUCCESS) {
+ uninstallResult.setValue(
+ UninstallSuccess(resultIntent = intent, activityResultCode = Activity.RESULT_OK)
+ )
+ } else {
+ uninstallResult.setValue(
+ UninstallFailed(
+ returnResult = true,
+ resultIntent = intent,
+ activityResultCode = Activity.RESULT_FIRST_USER
+ )
+ )
+ }
+ return
+ }
+
+ // Caller did not want the result back. So, we either show a Toast, or a Notification.
+ if (status == PackageInstaller.STATUS_SUCCESS) {
+ val statusMessage = if (isClonedApp) context.getString(
+ R.string.uninstall_done_clone_app, targetAppLabel
+ ) else context.getString(R.string.uninstall_done_app, targetAppLabel)
+ uninstallResult.setValue(
+ UninstallSuccess(activityResultCode = legacyStatus, message = statusMessage)
+ )
+ } else {
+ val uninstallFailureChannel = NotificationChannel(
+ UNINSTALL_FAILURE_CHANNEL,
+ context.getString(R.string.uninstall_failure_notification_channel),
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+ notificationManager!!.createNotificationChannel(uninstallFailureChannel)
+
+ val uninstallFailedNotification: Notification.Builder =
+ Notification.Builder(context, UNINSTALL_FAILURE_CHANNEL)
+
+ val myUserHandle = Process.myUserHandle()
+ when (legacyStatus) {
+ PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> {
+ // Find out if the package is an active admin for some non-current user.
+ val otherBlockingUserHandle =
+ findUserOfDeviceAdmin(myUserHandle, targetPackageName!!)
+ if (otherBlockingUserHandle == null) {
+ Log.d(
+ LOG_TAG, "Uninstall failed because $targetPackageName"
+ + " is a device admin"
+ )
+ addDeviceManagerButton(context, uninstallFailedNotification)
+ setBigText(
+ uninstallFailedNotification, context.getString(
+ R.string.uninstall_failed_device_policy_manager
+ )
+ )
+ } else {
+ Log.d(
+ LOG_TAG, "Uninstall failed because $targetPackageName"
+ + " is a device admin of user $otherBlockingUserHandle"
+ )
+ val userName = context.createContextAsUser(otherBlockingUserHandle, 0)
+ .getSystemService(UserManager::class.java)!!.userName
+ setBigText(
+ uninstallFailedNotification, String.format(
+ context.getString(
+ R.string.uninstall_failed_device_policy_manager_of_user
+ ), userName
+ )
+ )
+ }
+ }
+
+ PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
+ val otherBlockingUserHandle = findBlockingUser(targetPackageName!!)
+ val isProfileOfOrSame = isProfileOfOrSame(
+ userManager!!, myUserHandle, otherBlockingUserHandle
+ )
+ if (isProfileOfOrSame) {
+ addDeviceManagerButton(context, uninstallFailedNotification)
+ } else {
+ addManageUsersButton(context, uninstallFailedNotification)
+ }
+ var bigText: String? = null
+ if (otherBlockingUserHandle == null) {
+ Log.d(
+ LOG_TAG, "Uninstall failed for $targetPackageName " +
+ "with code $status no blocking user"
+ )
+ } else if (otherBlockingUserHandle === UserHandle.SYSTEM) {
+ bigText = context.getString(R.string.uninstall_blocked_device_owner)
+ } else {
+ bigText = context.getString(
+ if (uninstallFromAllUsers) R.string.uninstall_all_blocked_profile_owner
+ else R.string.uninstall_blocked_profile_owner
+ )
+ }
+ bigText?.let { setBigText(uninstallFailedNotification, it) }
+ }
+
+ else -> {
+ Log.d(
+ LOG_TAG, "Uninstall blocked for $targetPackageName"
+ + " with legacy code $legacyStatus"
+ )
+ }
+ }
+ uninstallFailedNotification.setContentTitle(
+ context.getString(R.string.uninstall_failed_app, targetAppLabel)
+ )
+ uninstallFailedNotification.setOngoing(false)
+ uninstallFailedNotification.setSmallIcon(R.drawable.ic_error)
+
+ uninstallResult.setValue(
+ UninstallFailed(
+ returnResult = false,
+ uninstallNotificationId = uninstallId,
+ uninstallNotification = uninstallFailedNotification.build()
+ )
+ )
+ }
+ }
+
+ /**
+ * @param myUserHandle [UserHandle] of the current user.
+ * @param packageName Name of the package being uninstalled.
+ * @return the [UserHandle] of the user in which a package is a device admin.
+ */
+ private fun findUserOfDeviceAdmin(myUserHandle: UserHandle, packageName: String): UserHandle? {
+ for (otherUserHandle in userManager!!.getUserHandles(true)) {
+ // We only catch the case when the user in question is neither the
+ // current user nor its profile.
+ if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) {
+ continue
+ }
+ val dpm = context.createContextAsUser(otherUserHandle, 0)
+ .getSystemService(DevicePolicyManager::class.java)
+ if (dpm!!.packageHasActiveAdmins(packageName)) {
+ return otherUserHandle
+ }
+ }
+ return null
+ }
+
+ /**
+ *
+ * @param packageName Name of the package being uninstalled.
+ * @return [UserHandle] of the user in which a package is blocked from being uninstalled.
+ */
+ private fun findBlockingUser(packageName: String): UserHandle? {
+ for (otherUserHandle in userManager!!.getUserHandles(true)) {
+ // TODO (b/307399586): Add a negation when the logic of the method is fixed
+ if (packageManager.canUserUninstall(packageName, otherUserHandle)) {
+ return otherUserHandle
+ }
+ }
+ return null
+ }
+
+ /**
+ * Set big text for the notification.
+ *
+ * @param builder The builder of the notification
+ * @param text The text to set.
+ */
+ private fun setBigText(
+ builder: Notification.Builder,
+ text: CharSequence
+ ) {
+ builder.setStyle(Notification.BigTextStyle().bigText(text))
+ }
+
+ /**
+ * Add a button to the notification that links to the user management.
+ *
+ * @param context The context the notification is created in
+ * @param builder The builder of the notification
+ */
+ private fun addManageUsersButton(
+ context: Context,
+ builder: Notification.Builder
+ ) {
+ builder.addAction(
+ Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
+ context.getString(R.string.manage_users),
+ PendingIntent.getActivity(
+ context, 0, getUserSettingsIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+ )
+ }
+
+ private fun getUserSettingsIntent(): Intent {
+ val intent = Intent(Settings.ACTION_USER_SETTINGS)
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK)
+ return intent
+ }
+
+ /**
+ * Add a button to the notification that links to the device policy management.
+ *
+ * @param context The context the notification is created in
+ * @param builder The builder of the notification
+ */
+ private fun addDeviceManagerButton(
+ context: Context,
+ builder: Notification.Builder
+ ) {
+ builder.addAction(
+ Notification.Action.Builder(
+ Icon.createWithResource(context, R.drawable.ic_lock),
+ context.getString(R.string.manage_device_administrators),
+ PendingIntent.getActivity(
+ context, 0, getDeviceManagerIntent(),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+ )
+ }
+
+ private fun getDeviceManagerIntent(): Intent {
+ val intent = Intent()
+ intent.setClassName(
+ "com.android.settings",
+ "com.android.settings.Settings\$DeviceAdminSettingsActivity"
+ )
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_TASK)
+ return intent
+ }
+
+ /**
+ * Starts an uninstall for the given package.
+ *
+ * @return `true` if there was no exception while uninstalling. This does not represent
+ * the result of the uninstall. Result will be made available in [handleUninstallResult]
+ */
+ private fun startUninstall(
+ packageName: String,
+ targetUser: UserHandle,
+ pendingIntent: PendingIntent,
+ uninstallFromAllUsers: Boolean,
+ keepData: Boolean
+ ): Boolean {
+ var flags = if (uninstallFromAllUsers) PackageManager.DELETE_ALL_USERS else 0
+ flags = flags or if (keepData) PackageManager.DELETE_KEEP_DATA else 0
+
+ return try {
+ context.createContextAsUser(targetUser, 0)
+ .packageManager.packageInstaller.uninstall(
+ VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
+ flags, pendingIntent.intentSender
+ )
+ true
+ } catch (e: IllegalArgumentException) {
+ Log.e(LOG_TAG, "Failed to uninstall", e)
+ false
+ }
+ }
+
+ fun cancelInstall() {
+ if (callback != null) {
+ callback!!.onUninstallComplete(
+ targetPackageName!!,
+ PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"
+ )
+ }
+ }
+
+ companion object {
+ private val LOG_TAG = UninstallRepository::class.java.simpleName
+ private const val UNINSTALL_FAILURE_CHANNEL = "uninstall_failure"
+ private const val BROADCAST_ACTION = "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"
+ private const val EXTRA_UNINSTALL_ID = "com.android.packageinstaller.extra.UNINSTALL_ID"
+ private const val EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL"
+ private const val EXTRA_IS_CLONE_APP = "com.android.packageinstaller.extra.IS_CLONE_APP"
+ private const val EXTRA_PACKAGE_NAME =
+ "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME"
+ }
+
+ class CallerInfo(val activityName: String?, val uid: Int)
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt
new file mode 100644
index 0000000..f086209
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallStages.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ * https://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.packageinstaller.v2.model
+
+import android.app.Activity
+import android.app.Notification
+import android.content.Intent
+import com.android.packageinstaller.R
+
+sealed class UninstallStage(val stageCode: Int) {
+
+ companion object {
+ const val STAGE_DEFAULT = -1
+ const val STAGE_ABORTED = 0
+ const val STAGE_READY = 1
+ const val STAGE_USER_ACTION_REQUIRED = 2
+ const val STAGE_UNINSTALLING = 3
+ const val STAGE_SUCCESS = 4
+ const val STAGE_FAILED = 5
+ }
+}
+
+class UninstallReady : UninstallStage(STAGE_READY)
+
+data class UninstallUserActionRequired(
+ val title: String? = null,
+ val message: String? = null,
+ val appDataSize: Long = 0
+) : UninstallStage(STAGE_USER_ACTION_REQUIRED)
+
+data class UninstallUninstalling(val appLabel: CharSequence, val isCloneUser: Boolean) :
+ UninstallStage(STAGE_UNINSTALLING)
+
+data class UninstallSuccess(
+ val resultIntent: Intent? = null,
+ val activityResultCode: Int = 0,
+ val message: String? = null,
+) : UninstallStage(STAGE_SUCCESS)
+
+data class UninstallFailed(
+ val returnResult: Boolean,
+ /**
+ * If the caller wants the result back, the intent will hold the uninstall failure status code
+ * and legacy code.
+ */
+ val resultIntent: Intent? = null,
+ val activityResultCode: Int = Activity.RESULT_CANCELED,
+ /**
+ * ID used to show [uninstallNotification]
+ */
+ val uninstallNotificationId: Int? = null,
+ /**
+ * When the user does not request a result back, this notification will be shown indicating the
+ * reason for uninstall failure.
+ */
+ val uninstallNotification: Notification? = null,
+) : UninstallStage(STAGE_FAILED) {
+
+ init {
+ if (uninstallNotification != null && uninstallNotificationId == null) {
+ throw IllegalArgumentException(
+ "uninstallNotification cannot be set without uninstallNotificationId"
+ )
+ }
+ }
+}
+
+data class UninstallAborted(val abortReason: Int) : UninstallStage(STAGE_ABORTED) {
+
+ var dialogTitleResource = 0
+ var dialogTextResource = 0
+ val activityResultCode = Activity.RESULT_FIRST_USER
+
+ init {
+ when (abortReason) {
+ ABORT_REASON_APP_UNAVAILABLE -> {
+ dialogTitleResource = R.string.app_not_found_dlg_title
+ dialogTextResource = R.string.app_not_found_dlg_text
+ }
+
+ ABORT_REASON_USER_NOT_ALLOWED -> {
+ dialogTitleResource = 0
+ dialogTextResource = R.string.user_is_not_allowed_dlg_text
+ }
+
+ else -> {
+ dialogTitleResource = 0
+ dialogTextResource = R.string.generic_error_dlg_text
+ }
+ }
+ }
+
+ companion object {
+ const val ABORT_REASON_GENERIC_ERROR = 0
+ const val ABORT_REASON_APP_UNAVAILABLE = 1
+ const val ABORT_REASON_USER_NOT_ALLOWED = 2
+ }
+}
+
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
deleted file mode 100644
index 520b6c5..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-
-import android.app.Activity;
-import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class InstallAborted extends InstallStage {
-
- public static final int ABORT_REASON_INTERNAL_ERROR = 0;
- public static final int ABORT_REASON_POLICY = 1;
- public static final int ABORT_REASON_DONE = 2;
- public static final int DLG_PACKAGE_ERROR = 1;
- private final int mStage = InstallStage.STAGE_ABORTED;
- private final int mAbortReason;
-
- /**
- * It will hold the restriction name, when the restriction was enforced by the system, and not
- * a device admin.
- */
- @NonNull
- private final String mMessage;
- /**
- * <p>If abort reason is ABORT_REASON_POLICY, then this will hold the Intent
- * to display a support dialog when a feature was disabled by an admin. It will be
- * {@code null} if the feature is disabled by the system. In this case, the restriction name
- * will be set in {@link #mMessage} </p>
- *
- * <p>If the abort reason is ABORT_REASON_INTERNAL_ERROR, it <b>may</b> hold an
- * intent to be sent as a result to the calling activity.</p>
- */
- @Nullable
- private final Intent mIntent;
- private final int mErrorDialogType;
- private final int mActivityResultCode;
-
- private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent,
- int activityResultCode, int errorDialogType) {
- mAbortReason = reason;
- mMessage = message;
- mIntent = intent;
- mErrorDialogType = errorDialogType;
- mActivityResultCode = activityResultCode;
- }
-
- public int getAbortReason() {
- return mAbortReason;
- }
-
- @NonNull
- public String getMessage() {
- return mMessage;
- }
-
- @Nullable
- public Intent getResultIntent() {
- return mIntent;
- }
-
- public int getErrorDialogType() {
- return mErrorDialogType;
- }
-
- public int getActivityResultCode() {
- return mActivityResultCode;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- public static class Builder {
-
- private final int mAbortReason;
- private String mMessage = "";
- private Intent mIntent = null;
- private int mActivityResultCode = Activity.RESULT_CANCELED;
- private int mErrorDialogType;
-
- public Builder(int reason) {
- mAbortReason = reason;
- }
-
- public Builder setMessage(@NonNull String message) {
- mMessage = message;
- return this;
- }
-
- public Builder setResultIntent(@NonNull Intent intent) {
- mIntent = intent;
- return this;
- }
-
- public Builder setErrorDialogType(int dialogType) {
- mErrorDialogType = dialogType;
- return this;
- }
-
- public Builder setActivityResultCode(int resultCode) {
- mActivityResultCode = resultCode;
- return this;
- }
-
- public InstallAborted build() {
- return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode,
- mErrorDialogType);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java
deleted file mode 100644
index 67e1690..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
-
-public class InstallFailed extends InstallStage {
-
- private final int mStage = InstallStage.STAGE_FAILED;
- @NonNull
- private final AppSnippet mAppSnippet;
- private final int mStatusCode;
- private final int mLegacyCode;
- @Nullable
- private final String mMessage;
-
- public InstallFailed(@NonNull AppSnippet appSnippet, int statusCode, int legacyCode,
- @Nullable String message) {
- mAppSnippet = appSnippet;
- mLegacyCode = statusCode;
- mStatusCode = legacyCode;
- mMessage = message;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- @NonNull
- public Drawable getAppIcon() {
- return mAppSnippet.getIcon();
- }
-
- @NonNull
- public String getAppLabel() {
- return (String) mAppSnippet.getLabel();
- }
-
- public int getStatusCode() {
- return mStatusCode;
- }
-
- public int getLegacyCode() {
- return mLegacyCode;
- }
-
- @Nullable
- public String getMessage() {
- return mMessage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java
deleted file mode 100644
index efd4947..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
-
-public class InstallInstalling extends InstallStage {
-
- private final int mStage = InstallStage.STAGE_INSTALLING;
- @NonNull
- private final AppSnippet mAppSnippet;
-
- public InstallInstalling(@NonNull AppSnippet appSnippet) {
- mAppSnippet = appSnippet;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- @NonNull
- public Drawable getAppIcon() {
- return mAppSnippet.getIcon();
- }
-
- @NonNull
- public String getAppLabel() {
- return (String) mAppSnippet.getLabel();
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
deleted file mode 100644
index 548f2c5..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallReady.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.installstagedata;
-
-public class InstallReady extends InstallStage{
-
- private final int mStage = InstallStage.STAGE_READY;
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
deleted file mode 100644
index f91e64b..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStage.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-public abstract class InstallStage {
-
- public static final int STAGE_DEFAULT = -1;
- public static final int STAGE_ABORTED = 0;
- public static final int STAGE_STAGING = 1;
- public static final int STAGE_READY = 2;
- public static final int STAGE_USER_ACTION_REQUIRED = 3;
- public static final int STAGE_INSTALLING = 4;
- public static final int STAGE_SUCCESS = 5;
- public static final int STAGE_FAILED = 6;
-
- /**
- * @return the integer value representing current install stage.
- */
- public abstract int getStageCode();
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
deleted file mode 100644
index a979cf8..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallStaging.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-public class InstallStaging extends InstallStage {
-
- private final int mStage = InstallStage.STAGE_STAGING;
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java
deleted file mode 100644
index da48256..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import androidx.annotation.NonNull;
-import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
-
-public class InstallSuccess extends InstallStage {
-
- private final int mStage = InstallStage.STAGE_SUCCESS;
-
- @NonNull
- private final AppSnippet mAppSnippet;
- private final boolean mShouldReturnResult;
- /**
- * <p>If the caller is requesting a result back, this will hold the Intent with
- * EXTRA_INSTALL_RESULT set to INSTALL_SUCCEEDED which is sent back to the caller.</p>
- * <p>If the caller doesn't want the result back, this will hold the Intent that launches
- * the newly installed / updated app.</p>
- */
- @NonNull
- private final Intent mResultIntent;
-
- public InstallSuccess(@NonNull AppSnippet appSnippet, boolean shouldReturnResult,
- @NonNull Intent launcherIntent) {
- mAppSnippet = appSnippet;
- mShouldReturnResult = shouldReturnResult;
- mResultIntent = launcherIntent;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- @NonNull
- public Drawable getAppIcon() {
- return mAppSnippet.getIcon();
- }
-
- @NonNull
- public String getAppLabel() {
- return (String) mAppSnippet.getLabel();
- }
-
- public boolean shouldReturnResult() {
- return mShouldReturnResult;
- }
-
- @NonNull
- public Intent getResultIntent() {
- return mResultIntent;
- }
-
- public static class Builder {
-
- private final AppSnippet mAppSnippet;
- private boolean mShouldReturnResult;
- private Intent mLauncherIntent;
-
- public Builder(@NonNull AppSnippet appSnippet) {
- mAppSnippet = appSnippet;
- }
-
- public Builder setShouldReturnResult(boolean returnResult) {
- mShouldReturnResult = returnResult;
- return this;
- }
-
- public Builder setResultIntent(@NonNull Intent intent) {
- mLauncherIntent = intent;
- return this;
- }
-
- public InstallSuccess build() {
- return new InstallSuccess(mAppSnippet, mShouldReturnResult, mLauncherIntent);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java
deleted file mode 100644
index 08a7487..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.installstagedata;
-
-import android.graphics.drawable.Drawable;
-import androidx.annotation.Nullable;
-import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
-
-public class InstallUserActionRequired extends InstallStage {
-
- public static final int USER_ACTION_REASON_UNKNOWN_SOURCE = 0;
- public static final int USER_ACTION_REASON_ANONYMOUS_SOURCE = 1;
- public static final int USER_ACTION_REASON_INSTALL_CONFIRMATION = 2;
- private final int mStage = InstallStage.STAGE_USER_ACTION_REQUIRED;
- private final int mActionReason;
- @Nullable
- private final AppSnippet mAppSnippet;
- private final boolean mIsAppUpdating;
- @Nullable
- private final String mDialogMessage;
-
- public InstallUserActionRequired(int actionReason, @Nullable AppSnippet appSnippet,
- boolean isUpdating, @Nullable String dialogMessage) {
- mActionReason = actionReason;
- mAppSnippet = appSnippet;
- mIsAppUpdating = isUpdating;
- mDialogMessage = dialogMessage;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- @Nullable
- public Drawable getAppIcon() {
- return mAppSnippet != null ? mAppSnippet.getIcon() : null;
- }
-
- @Nullable
- public String getAppLabel() {
- return mAppSnippet != null ? (String) mAppSnippet.getLabel() : null;
- }
-
- public boolean isAppUpdating() {
- return mIsAppUpdating;
- }
-
- @Nullable
- public String getDialogMessage() {
- return mDialogMessage;
- }
-
- public int getActionReason() {
- return mActionReason;
- }
-
- public static class Builder {
-
- private final int mActionReason;
- private final AppSnippet mAppSnippet;
- private boolean mIsAppUpdating;
- private String mDialogMessage;
-
- public Builder(int actionReason, @Nullable AppSnippet appSnippet) {
- mActionReason = actionReason;
- mAppSnippet = appSnippet;
- }
-
- public Builder setAppUpdating(boolean isUpdating) {
- mIsAppUpdating = isUpdating;
- return this;
- }
-
- public Builder setDialogMessage(@Nullable String message) {
- mDialogMessage = message;
- return this;
- }
-
- public InstallUserActionRequired build() {
- return new InstallUserActionRequired(mActionReason, mAppSnippet, mIsAppUpdating,
- mDialogMessage);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
deleted file mode 100644
index 9aea6b1..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-import android.app.Activity;
-import com.android.packageinstaller.R;
-
-public class UninstallAborted extends UninstallStage {
-
- public static final int ABORT_REASON_GENERIC_ERROR = 0;
- public static final int ABORT_REASON_APP_UNAVAILABLE = 1;
- public static final int ABORT_REASON_USER_NOT_ALLOWED = 2;
- private final int mStage = UninstallStage.STAGE_ABORTED;
- private final int mAbortReason;
- private final int mDialogTitleResource;
- private final int mDialogTextResource;
- private final int mActivityResultCode = Activity.RESULT_FIRST_USER;
-
- public UninstallAborted(int abortReason) {
- mAbortReason = abortReason;
- switch (abortReason) {
- case ABORT_REASON_APP_UNAVAILABLE -> {
- mDialogTitleResource = R.string.app_not_found_dlg_title;
- mDialogTextResource = R.string.app_not_found_dlg_text;
- }
- case ABORT_REASON_USER_NOT_ALLOWED -> {
- mDialogTitleResource = 0;
- mDialogTextResource = R.string.user_is_not_allowed_dlg_text;
- }
- default -> {
- mDialogTitleResource = 0;
- mDialogTextResource = R.string.generic_error_dlg_text;
- }
- }
- }
-
- public int getAbortReason() {
- return mAbortReason;
- }
-
- public int getActivityResultCode() {
- return mActivityResultCode;
- }
-
- public int getDialogTitleResource() {
- return mDialogTitleResource;
- }
-
- public int getDialogTextResource() {
- return mDialogTextResource;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
deleted file mode 100644
index 6ed8883..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-import android.app.Activity;
-import android.app.Notification;
-import android.content.Intent;
-
-public class UninstallFailed extends UninstallStage {
-
- private final int mStage = UninstallStage.STAGE_FAILED;
- private final boolean mReturnResult;
- /**
- * If the caller wants the result back, the intent will hold the uninstall failure status code
- * and legacy code.
- */
- private final Intent mResultIntent;
- /**
- * When the user does not request a result back, this notification will be shown indicating the
- * reason for uninstall failure.
- */
- private final Notification mUninstallNotification;
- /**
- * ID used to show {@link #mUninstallNotification}
- */
- private final int mUninstallId;
- private final int mActivityResultCode;
-
- public UninstallFailed(boolean returnResult, Intent resultIntent, int activityResultCode,
- int uninstallId, Notification uninstallNotification) {
- mReturnResult = returnResult;
- mResultIntent = resultIntent;
- mActivityResultCode = activityResultCode;
- mUninstallId = uninstallId;
- mUninstallNotification = uninstallNotification;
- }
-
- public boolean returnResult() {
- return mReturnResult;
- }
-
- public Intent getResultIntent() {
- return mResultIntent;
- }
-
- public int getActivityResultCode() {
- return mActivityResultCode;
- }
-
- public Notification getUninstallNotification() {
- return mUninstallNotification;
- }
-
- public int getUninstallId() {
- return mUninstallId;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- public static class Builder {
-
- private final boolean mReturnResult;
- private int mActivityResultCode = Activity.RESULT_CANCELED;
- /**
- * See {@link UninstallFailed#mResultIntent}
- */
- private Intent mResultIntent = null;
- /**
- * See {@link UninstallFailed#mUninstallNotification}
- */
- private Notification mUninstallNotification;
- /**
- * See {@link UninstallFailed#mUninstallId}
- */
- private int mUninstallId;
-
- public Builder(boolean returnResult) {
- mReturnResult = returnResult;
- }
-
- public Builder setUninstallNotification(int uninstallId, Notification notification) {
- mUninstallId = uninstallId;
- mUninstallNotification = notification;
- return this;
- }
-
- public Builder setResultIntent(Intent intent) {
- mResultIntent = intent;
- return this;
- }
-
- public Builder setActivityResultCode(int resultCode) {
- mActivityResultCode = resultCode;
- return this;
- }
-
- public UninstallFailed build() {
- return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode,
- mUninstallId, mUninstallNotification);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
deleted file mode 100644
index 0108cb4..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-public class UninstallReady extends UninstallStage {
-
- private final int mStage = UninstallStage.STAGE_READY;
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java
deleted file mode 100644
index 87ca4ec..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallStage.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.model.uninstallstagedata;
-
-public abstract class UninstallStage {
-
- public static final int STAGE_DEFAULT = -1;
- public static final int STAGE_ABORTED = 0;
- public static final int STAGE_READY = 1;
- public static final int STAGE_USER_ACTION_REQUIRED = 2;
- public static final int STAGE_UNINSTALLING = 3;
- public static final int STAGE_SUCCESS = 4;
- public static final int STAGE_FAILED = 5;
-
- public abstract int getStageCode();
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
deleted file mode 100644
index 5df6b02..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-import android.content.Intent;
-
-public class UninstallSuccess extends UninstallStage {
-
- private final int mStage = UninstallStage.STAGE_SUCCESS;
- private final String mMessage;
- private final Intent mResultIntent;
- private final int mActivityResultCode;
-
- public UninstallSuccess(Intent resultIntent, int activityResultCode, String message) {
- mResultIntent = resultIntent;
- mActivityResultCode = activityResultCode;
- mMessage = message;
- }
-
- public String getMessage() {
- return mMessage;
- }
-
- public Intent getResultIntent() {
- return mResultIntent;
- }
-
- public int getActivityResultCode() {
- return mActivityResultCode;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- public static class Builder {
-
- private Intent mResultIntent;
- private int mActivityResultCode;
- private String mMessage;
-
- public Builder() {
- }
-
- public Builder setResultIntent(Intent intent) {
- mResultIntent = intent;
- return this;
- }
-
- public Builder setActivityResultCode(int resultCode) {
- mActivityResultCode = resultCode;
- return this;
- }
-
- public Builder setMessage(String message) {
- mMessage = message;
- return this;
- }
-
- public UninstallSuccess build() {
- return new UninstallSuccess(mResultIntent, mActivityResultCode, mMessage);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
deleted file mode 100644
index f5156cb..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-public class UninstallUninstalling extends UninstallStage {
-
- private final int mStage = UninstallStage.STAGE_UNINSTALLING;
-
- private final CharSequence mAppLabel;
- private final boolean mIsCloneUser;
-
- public UninstallUninstalling(CharSequence appLabel, boolean isCloneUser) {
- mAppLabel = appLabel;
- mIsCloneUser = isCloneUser;
- }
-
- public CharSequence getAppLabel() {
- return mAppLabel;
- }
-
- public boolean isCloneUser() {
- return mIsCloneUser;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
deleted file mode 100644
index b600149..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.model.uninstallstagedata;
-
-public class UninstallUserActionRequired extends UninstallStage {
-
- private final int mStage = UninstallStage.STAGE_USER_ACTION_REQUIRED;
- private final String mTitle;
- private final String mMessage;
- private final long mAppDataSize;
-
- public UninstallUserActionRequired(String title, String message, long appDataSize) {
- mTitle = title;
- mMessage = message;
- mAppDataSize = appDataSize;
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public String getMessage() {
- return mMessage;
- }
-
- public long getAppDataSize() {
- return mAppDataSize;
- }
-
- @Override
- public int getStageCode() {
- return mStage;
- }
-
- public static class Builder {
-
- private String mTitle;
- private String mMessage;
- private long mAppDataSize = 0;
-
- public Builder setTitle(String title) {
- mTitle = title;
- return this;
- }
-
- public Builder setMessage(String message) {
- mMessage = message;
- return this;
- }
-
- public Builder setAppDataSize(long appDataSize) {
- mAppDataSize = appDataSize;
- return this;
- }
-
- public UninstallUserActionRequired build() {
- return new UninstallUserActionRequired(mTitle, mMessage, mAppDataSize);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java
deleted file mode 100644
index fdb024f..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.ui;
-
-import android.content.Intent;
-
-public interface InstallActionListener {
-
- /**
- * Method to handle a positive response from the user
- */
- void onPositiveResponse(int stageCode);
-
- /**
- * Method to dispatch intent for toggling "install from unknown sources" setting for a package
- */
- void sendUnknownAppsIntent(String packageName);
-
- /**
- * Method to handle a negative response from the user
- */
- void onNegativeResponse(int stageCode);
- void openInstalledApp(Intent intent);
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt
new file mode 100644
index 0000000..c109fc6
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.ui
+
+import android.content.Intent
+
+interface InstallActionListener {
+ /**
+ * Method to handle a positive response from the user.
+ */
+ fun onPositiveResponse(reasonCode: Int)
+
+ /**
+ * Method to dispatch intent for toggling "install from unknown sources" setting for a package.
+ */
+ fun sendUnknownAppsIntent(sourcePackageName: String)
+
+ /**
+ * Method to handle a negative response from the user.
+ */
+ fun onNegativeResponse(stageCode: Int)
+
+ /**
+ * Launch the intent to open the newly installed / updated app.
+ */
+ fun openInstalledApp(intent: Intent?)
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
deleted file mode 100644
index d06b4b3..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
+++ /dev/null
@@ -1,354 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.ui;
-
-import static android.content.Intent.CATEGORY_LAUNCHER;
-import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
-import static android.os.Process.INVALID_UID;
-import static com.android.packageinstaller.v2.model.InstallRepository.EXTRA_STAGED_SESSION_ID;
-
-import android.app.Activity;
-import android.app.AppOpsManager;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.UserManager;
-import android.provider.Settings;
-import android.util.Log;
-import android.view.Window;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.lifecycle.ViewModelProvider;
-import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.InstallRepository;
-import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
-import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
-import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
-import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
-import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
-import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
-import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment;
-import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment;
-import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment;
-import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment;
-import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment;
-import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment;
-import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment;
-import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment;
-import com.android.packageinstaller.v2.viewmodel.InstallViewModel;
-import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory;
-import java.util.ArrayList;
-import java.util.List;
-
-public class InstallLaunch extends FragmentActivity implements InstallActionListener {
-
- public static final String EXTRA_CALLING_PKG_UID =
- InstallLaunch.class.getPackageName() + ".callingPkgUid";
- public static final String EXTRA_CALLING_PKG_NAME =
- InstallLaunch.class.getPackageName() + ".callingPkgName";
- private static final String TAG = InstallLaunch.class.getSimpleName();
- private static final String TAG_DIALOG = "dialog";
- private final int REQUEST_TRUST_EXTERNAL_SOURCE = 1;
- private final boolean mLocalLOGV = false;
- /**
- * A collection of unknown sources listeners that are actively listening for app ops mode
- * changes
- */
- private final List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1);
- private InstallViewModel mInstallViewModel;
- private InstallRepository mInstallRepository;
- private FragmentManager mFragmentManager;
- private AppOpsManager mAppOpsManager;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- this.requestWindowFeature(Window.FEATURE_NO_TITLE);
-
- mFragmentManager = getSupportFragmentManager();
- mAppOpsManager = getSystemService(AppOpsManager.class);
-
- mInstallRepository = new InstallRepository(getApplicationContext());
- mInstallViewModel = new ViewModelProvider(this,
- new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get(
- InstallViewModel.class);
-
- Intent intent = getIntent();
- CallerInfo info = new CallerInfo(
- intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
- intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
- mInstallViewModel.preprocessIntent(intent, info);
-
- mInstallViewModel.getCurrentInstallStage().observe(this, this::onInstallStageChange);
- }
-
- /**
- * Main controller of the UI. This method shows relevant dialogs based on the install stage
- */
- private void onInstallStageChange(InstallStage installStage) {
- switch (installStage.getStageCode()) {
- case InstallStage.STAGE_STAGING -> {
- InstallStagingFragment stagingDialog = new InstallStagingFragment();
- showDialogInner(stagingDialog);
- mInstallViewModel.getStagingProgress().observe(this, stagingDialog::setProgress);
- }
- case InstallStage.STAGE_ABORTED -> {
- InstallAborted aborted = (InstallAborted) installStage;
- switch (aborted.getAbortReason()) {
- // TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR
- case InstallAborted.ABORT_REASON_DONE,
- InstallAborted.ABORT_REASON_INTERNAL_ERROR ->
- setResult(aborted.getActivityResultCode(), aborted.getResultIntent(), true);
- case InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
- default -> setResult(RESULT_CANCELED, null, true);
- }
- }
- case InstallStage.STAGE_USER_ACTION_REQUIRED -> {
- InstallUserActionRequired uar = (InstallUserActionRequired) installStage;
- switch (uar.getActionReason()) {
- case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> {
- InstallConfirmationFragment actionDialog =
- new InstallConfirmationFragment(uar);
- showDialogInner(actionDialog);
- }
- case InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> {
- ExternalSourcesBlockedFragment externalSourceDialog =
- new ExternalSourcesBlockedFragment(uar);
- showDialogInner(externalSourceDialog);
- }
- case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> {
- AnonymousSourceFragment anonymousSourceDialog =
- new AnonymousSourceFragment();
- showDialogInner(anonymousSourceDialog);
- }
- }
- }
- case InstallStage.STAGE_INSTALLING -> {
- InstallInstalling installing = (InstallInstalling) installStage;
- InstallInstallingFragment installingDialog =
- new InstallInstallingFragment(installing);
- showDialogInner(installingDialog);
- }
- case InstallStage.STAGE_SUCCESS -> {
- InstallSuccess success = (InstallSuccess) installStage;
- if (success.shouldReturnResult()) {
- Intent successIntent = success.getResultIntent();
- setResult(Activity.RESULT_OK, successIntent, true);
- } else {
- InstallSuccessFragment successFragment = new InstallSuccessFragment(success);
- showDialogInner(successFragment);
- }
- }
- case InstallStage.STAGE_FAILED -> {
- InstallFailed failed = (InstallFailed) installStage;
- InstallFailedFragment failedDialog = new InstallFailedFragment(failed);
- showDialogInner(failedDialog);
- }
- default -> {
- Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode());
- showDialogInner(null);
- }
- }
- }
-
- private void showPolicyRestrictionDialog(InstallAborted aborted) {
- String restriction = aborted.getMessage();
- Intent adminSupportIntent = aborted.getResultIntent();
- boolean shouldFinish;
-
- // If the given restriction is set by an admin, display information about the
- // admin enforcing the restriction for the affected user. If not enforced by the admin,
- // show the system dialog.
- if (adminSupportIntent != null) {
- if (mLocalLOGV) {
- Log.i(TAG, "Restriction set by admin, starting " + adminSupportIntent);
- }
- startActivity(adminSupportIntent);
- // Finish the package installer app since the next dialog will not be shown by this app
- shouldFinish = true;
- } else {
- if (mLocalLOGV) {
- Log.i(TAG, "Restriction set by system: " + restriction);
- }
- DialogFragment blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction);
- // Don't finish the package installer app since the next dialog
- // will be shown by this app
- shouldFinish = false;
- showDialogInner(blockedByPolicyDialog);
- }
- setResult(RESULT_CANCELED, null, shouldFinish);
- }
-
- /**
- * Create a new dialog based on the install restriction enforced.
- *
- * @param restriction The restriction to create the dialog for
- * @return The dialog
- */
- private DialogFragment createDevicePolicyRestrictionDialog(String restriction) {
- if (mLocalLOGV) {
- Log.i(TAG, "createDialog(" + restriction + ")");
- }
- return switch (restriction) {
- case UserManager.DISALLOW_INSTALL_APPS ->
- new SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text);
- case UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
- UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
- new SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text);
- default -> null;
- };
- }
-
- /**
- * Replace any visible dialog by the dialog returned by InstallRepository
- *
- * @param newDialog The new dialog to display
- */
- private void showDialogInner(@Nullable DialogFragment newDialog) {
- DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
- TAG_DIALOG);
- if (currentDialog != null) {
- currentDialog.dismissAllowingStateLoss();
- }
- if (newDialog != null) {
- newDialog.show(mFragmentManager, TAG_DIALOG);
- }
- }
-
- public void setResult(int resultCode, Intent data, boolean shouldFinish) {
- super.setResult(resultCode, data);
- if (shouldFinish) {
- finish();
- }
- }
-
- @Override
- public void onPositiveResponse(int reasonCode) {
- switch (reasonCode) {
- case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE ->
- mInstallViewModel.forcedSkipSourceCheck();
- case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION ->
- mInstallViewModel.initiateInstall();
- }
- }
-
- @Override
- public void onNegativeResponse(int stageCode) {
- if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
- mInstallViewModel.cleanupInstall();
- }
- setResult(Activity.RESULT_CANCELED, null, true);
- }
-
- @Override
- public void sendUnknownAppsIntent(String sourcePackageName) {
- Intent settingsIntent = new Intent();
- settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
- final Uri packageUri = Uri.parse("package:" + sourcePackageName);
- settingsIntent.setData(packageUri);
- settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY);
-
- try {
- registerAppOpChangeListener(new UnknownSourcesListener(sourcePackageName),
- sourcePackageName);
- startActivityForResult(settingsIntent, REQUEST_TRUST_EXTERNAL_SOURCE);
- } catch (ActivityNotFoundException exc) {
- Log.e(TAG, "Settings activity not found for action: "
- + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
- }
- }
-
- @Override
- public void openInstalledApp(Intent intent) {
- setResult(RESULT_OK, intent, true);
- if (intent != null && intent.hasCategory(CATEGORY_LAUNCHER)) {
- startActivity(intent);
- }
- }
-
- private void registerAppOpChangeListener(UnknownSourcesListener listener, String packageName) {
- mAppOpsManager.startWatchingMode(
- AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, packageName,
- listener);
- mActiveUnknownSourcesListeners.add(listener);
- }
-
- private void unregisterAppOpChangeListener(UnknownSourcesListener listener) {
- mActiveUnknownSourcesListeners.remove(listener);
- mAppOpsManager.stopWatchingMode(listener);
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_TRUST_EXTERNAL_SOURCE) {
- mInstallViewModel.reattemptInstall();
- } else {
- setResult(Activity.RESULT_CANCELED, null, true);
- }
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- while (!mActiveUnknownSourcesListeners.isEmpty()) {
- unregisterAppOpChangeListener(mActiveUnknownSourcesListeners.get(0));
- }
- }
-
- private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener {
-
- private final String mOriginatingPackage;
-
- public UnknownSourcesListener(String originatingPackage) {
- mOriginatingPackage = originatingPackage;
- }
-
- @Override
- public void onOpChanged(String op, String packageName) {
- if (!mOriginatingPackage.equals(packageName)) {
- return;
- }
- unregisterAppOpChangeListener(this);
- mActiveUnknownSourcesListeners.remove(this);
- if (isDestroyed()) {
- return;
- }
- new Handler(Looper.getMainLooper()).postDelayed(() -> {
- if (!isDestroyed()) {
- // Relaunch Pia to continue installation.
- startActivity(getIntent()
- .putExtra(EXTRA_STAGED_SESSION_ID, mInstallViewModel.getStagedSessionId()));
-
- // If the userId of the root of activity stack is different from current userId,
- // starting Pia again lead to duplicate instances of the app in the stack.
- // As such, finish the old instance. Old Pia is finished even if the userId of
- // the root is the same, since there is no way to determine the difference in
- // userIds.
- finish();
- }
- }, 500);
- }
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
new file mode 100644
index 0000000..2b610d7
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.ui
+
+import android.app.Activity
+import android.app.AppOpsManager
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.os.UserManager
+import android.provider.Settings
+import android.util.Log
+import android.view.Window
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.ViewModelProvider
+import com.android.packageinstaller.R
+import com.android.packageinstaller.v2.model.InstallRepository
+import com.android.packageinstaller.v2.model.InstallAborted
+import com.android.packageinstaller.v2.model.InstallFailed
+import com.android.packageinstaller.v2.model.InstallInstalling
+import com.android.packageinstaller.v2.model.InstallStage
+import com.android.packageinstaller.v2.model.InstallSuccess
+import com.android.packageinstaller.v2.model.InstallUserActionRequired
+import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment
+import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment
+import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment
+import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment
+import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment
+import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment
+import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment
+import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment
+import com.android.packageinstaller.v2.viewmodel.InstallViewModel
+import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory
+
+class InstallLaunch : FragmentActivity(), InstallActionListener {
+
+ companion object {
+ @JvmField val EXTRA_CALLING_PKG_UID =
+ InstallLaunch::class.java.packageName + ".callingPkgUid"
+ @JvmField val EXTRA_CALLING_PKG_NAME =
+ InstallLaunch::class.java.packageName + ".callingPkgName"
+ private val LOG_TAG = InstallLaunch::class.java.simpleName
+ private const val TAG_DIALOG = "dialog"
+ }
+
+ private val localLOGV = false
+
+ /**
+ * A collection of unknown sources listeners that are actively listening for app ops mode
+ * changes
+ */
+ private val activeUnknownSourcesListeners: MutableList<UnknownSourcesListener> = ArrayList(1)
+ private var installViewModel: InstallViewModel? = null
+ private var installRepository: InstallRepository? = null
+ private var fragmentManager: FragmentManager? = null
+ private var appOpsManager: AppOpsManager? = null
+ private lateinit var unknownAppsIntentLauncher: ActivityResultLauncher<Intent>
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ requestWindowFeature(Window.FEATURE_NO_TITLE)
+ fragmentManager = supportFragmentManager
+ appOpsManager = getSystemService(AppOpsManager::class.java)
+ installRepository = InstallRepository(applicationContext)
+ installViewModel = ViewModelProvider(
+ this, InstallViewModelFactory(this.application, installRepository!!)
+ )[InstallViewModel::class.java]
+
+ val intent = intent
+ val info = InstallRepository.CallerInfo(
+ intent.getStringExtra(EXTRA_CALLING_PKG_NAME),
+ intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID)
+ )
+ installViewModel!!.preprocessIntent(intent, info)
+ installViewModel!!.currentInstallStage.observe(this) { installStage: InstallStage ->
+ onInstallStageChange(installStage)
+ }
+
+ // Used to launch intent for Settings, to manage "install unknown apps" permission
+ unknownAppsIntentLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ // Reattempt installation on coming back from Settings, after toggling
+ // "install unknown apps" permission
+ installViewModel!!.reattemptInstall()
+ }
+ }
+
+ /**
+ * Main controller of the UI. This method shows relevant dialogs based on the install stage
+ */
+ private fun onInstallStageChange(installStage: InstallStage) {
+ when (installStage.stageCode) {
+ InstallStage.STAGE_STAGING -> {
+ val stagingDialog = InstallStagingFragment()
+ showDialogInner(stagingDialog)
+ installViewModel!!.stagingProgress.observe(this) { progress: Int ->
+ stagingDialog.setProgress(progress)
+ }
+ }
+
+ InstallStage.STAGE_ABORTED -> {
+ val aborted = installStage as InstallAborted
+ when (aborted.abortReason) {
+ InstallAborted.ABORT_REASON_DONE, InstallAborted.ABORT_REASON_INTERNAL_ERROR ->
+ setResult(aborted.activityResultCode, aborted.resultIntent, true)
+
+ InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted)
+ else -> setResult(Activity.RESULT_CANCELED, null, true)
+ }
+ }
+
+ InstallStage.STAGE_USER_ACTION_REQUIRED -> {
+ val uar = installStage as InstallUserActionRequired
+ when (uar.actionReason) {
+ InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION -> {
+ val actionDialog = InstallConfirmationFragment(uar)
+ showDialogInner(actionDialog)
+ }
+
+ InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE -> {
+ val externalSourceDialog = ExternalSourcesBlockedFragment(uar)
+ showDialogInner(externalSourceDialog)
+ }
+
+ InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE -> {
+ val anonymousSourceDialog = AnonymousSourceFragment()
+ showDialogInner(anonymousSourceDialog)
+ }
+ }
+ }
+
+ InstallStage.STAGE_INSTALLING -> {
+ val installing = installStage as InstallInstalling
+ val installingDialog = InstallInstallingFragment(installing)
+ showDialogInner(installingDialog)
+ }
+
+ InstallStage.STAGE_SUCCESS -> {
+ val success = installStage as InstallSuccess
+ if (success.shouldReturnResult) {
+ val successIntent = success.resultIntent
+ setResult(Activity.RESULT_OK, successIntent, true)
+ } else {
+ val successFragment = InstallSuccessFragment(success)
+ showDialogInner(successFragment)
+ }
+ }
+
+ InstallStage.STAGE_FAILED -> {
+ val failed = installStage as InstallFailed
+ val failedDialog = InstallFailedFragment(failed)
+ showDialogInner(failedDialog)
+ }
+
+ else -> {
+ Log.d(LOG_TAG, "Unimplemented stage: " + installStage.stageCode)
+ showDialogInner(null)
+ }
+ }
+ }
+
+ private fun showPolicyRestrictionDialog(aborted: InstallAborted) {
+ val restriction = aborted.message
+ val adminSupportIntent = aborted.resultIntent
+ var shouldFinish: Boolean = false
+
+ // If the given restriction is set by an admin, display information about the
+ // admin enforcing the restriction for the affected user. If not enforced by the admin,
+ // show the system dialog.
+ if (adminSupportIntent != null) {
+ if (localLOGV) {
+ Log.i(LOG_TAG, "Restriction set by admin, starting $adminSupportIntent")
+ }
+ startActivity(adminSupportIntent)
+ // Finish the package installer app since the next dialog will not be shown by this app
+ shouldFinish = true
+ } else {
+ if (localLOGV) {
+ Log.i(LOG_TAG, "Restriction set by system: $restriction")
+ }
+ val blockedByPolicyDialog = createDevicePolicyRestrictionDialog(restriction)
+ // Don't finish the package installer app since the next dialog
+ // will be shown by this app
+ shouldFinish = blockedByPolicyDialog != null
+ showDialogInner(blockedByPolicyDialog)
+ }
+ setResult(Activity.RESULT_CANCELED, null, shouldFinish)
+ }
+
+ /**
+ * Create a new dialog based on the install restriction enforced.
+ *
+ * @param restriction The restriction to create the dialog for
+ * @return The dialog
+ */
+ private fun createDevicePolicyRestrictionDialog(restriction: String?): DialogFragment? {
+ if (localLOGV) {
+ Log.i(LOG_TAG, "createDialog($restriction)")
+ }
+ return when (restriction) {
+ UserManager.DISALLOW_INSTALL_APPS ->
+ SimpleErrorFragment(R.string.install_apps_user_restriction_dlg_text)
+
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES,
+ UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY ->
+ SimpleErrorFragment(R.string.unknown_apps_user_restriction_dlg_text)
+
+ else -> null
+ }
+ }
+
+ /**
+ * Replace any visible dialog by the dialog returned by InstallRepository
+ *
+ * @param newDialog The new dialog to display
+ */
+ private fun showDialogInner(newDialog: DialogFragment?) {
+ val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment?
+ currentDialog?.dismissAllowingStateLoss()
+ newDialog?.show(fragmentManager!!, TAG_DIALOG)
+ }
+
+ fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
+ super.setResult(resultCode, data)
+ if (shouldFinish) {
+ finish()
+ }
+ }
+
+ override fun onPositiveResponse(reasonCode: Int) {
+ when (reasonCode) {
+ InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE ->
+ installViewModel!!.forcedSkipSourceCheck()
+
+ InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION ->
+ installViewModel!!.initiateInstall()
+ }
+ }
+
+ override fun onNegativeResponse(stageCode: Int) {
+ if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
+ installViewModel!!.cleanupInstall()
+ }
+ setResult(Activity.RESULT_CANCELED, null, true)
+ }
+
+ override fun sendUnknownAppsIntent(sourcePackageName: String) {
+ val settingsIntent = Intent()
+ settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
+ val packageUri = Uri.parse("package:$sourcePackageName")
+ settingsIntent.setData(packageUri)
+ settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ try {
+ registerAppOpChangeListener(
+ UnknownSourcesListener(sourcePackageName), sourcePackageName
+ )
+ unknownAppsIntentLauncher.launch(settingsIntent)
+ } catch (exc: ActivityNotFoundException) {
+ Log.e(
+ LOG_TAG, "Settings activity not found for action: "
+ + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
+ )
+ }
+ }
+
+ override fun openInstalledApp(intent: Intent?) {
+ setResult(Activity.RESULT_OK, intent, true)
+ if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
+ startActivity(intent)
+ }
+ }
+
+ private fun registerAppOpChangeListener(listener: UnknownSourcesListener, packageName: String) {
+ appOpsManager!!.startWatchingMode(
+ AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES,
+ packageName,
+ listener
+ )
+ activeUnknownSourcesListeners.add(listener)
+ }
+
+ private fun unregisterAppOpChangeListener(listener: UnknownSourcesListener) {
+ activeUnknownSourcesListeners.remove(listener)
+ appOpsManager!!.stopWatchingMode(listener)
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ while (activeUnknownSourcesListeners.isNotEmpty()) {
+ unregisterAppOpChangeListener(activeUnknownSourcesListeners[0])
+ }
+ }
+
+ private inner class UnknownSourcesListener(private val mOriginatingPackage: String) :
+ AppOpsManager.OnOpChangedListener {
+ override fun onOpChanged(op: String, packageName: String) {
+ if (mOriginatingPackage != packageName) {
+ return
+ }
+ unregisterAppOpChangeListener(this)
+ activeUnknownSourcesListeners.remove(this)
+ if (isDestroyed) {
+ return
+ }
+ Handler(Looper.getMainLooper()).postDelayed({
+ if (!isDestroyed) {
+ // Relaunch Pia to continue installation.
+ startActivity(
+ intent.putExtra(
+ InstallRepository.EXTRA_STAGED_SESSION_ID,
+ installViewModel!!.stagedSessionId
+ )
+ )
+
+ // If the userId of the root of activity stack is different from current userId,
+ // starting Pia again lead to duplicate instances of the app in the stack.
+ // As such, finish the old instance. Old Pia is finished even if the userId of
+ // the root is the same, since there is no way to determine the difference in
+ // userIds.
+ finish()
+ }
+ }, 500)
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt
similarity index 78%
rename from packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
rename to packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt
index b8a9355..33f5db3 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.kt
@@ -14,11 +14,9 @@
* limitations under the License.
*/
-package com.android.packageinstaller.v2.ui;
+package com.android.packageinstaller.v2.ui
-public interface UninstallActionListener {
-
- void onPositiveResponse(boolean keepData);
-
- void onNegativeResponse();
+interface UninstallActionListener {
+ fun onPositiveResponse(keepData: Boolean)
+ fun onNegativeResponse()
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
deleted file mode 100644
index 7638e91..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.ui;
-
-import static android.os.Process.INVALID_UID;
-import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
-
-import android.app.Activity;
-import android.app.NotificationManager;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.widget.Toast;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.fragment.app.FragmentManager;
-import androidx.lifecycle.ViewModelProvider;
-import com.android.packageinstaller.v2.model.UninstallRepository;
-import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
-import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment;
-import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment;
-import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment;
-import com.android.packageinstaller.v2.viewmodel.UninstallViewModel;
-import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory;
-
-public class UninstallLaunch extends FragmentActivity implements UninstallActionListener {
-
- public static final String EXTRA_CALLING_PKG_UID =
- UninstallLaunch.class.getPackageName() + ".callingPkgUid";
- public static final String EXTRA_CALLING_ACTIVITY_NAME =
- UninstallLaunch.class.getPackageName() + ".callingActivityName";
- public static final String TAG = UninstallLaunch.class.getSimpleName();
- private static final String TAG_DIALOG = "dialog";
-
- private UninstallViewModel mUninstallViewModel;
- private UninstallRepository mUninstallRepository;
- private FragmentManager mFragmentManager;
- private NotificationManager mNotificationManager;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
-
- // Never restore any state, esp. never create any fragments. The data in the fragment might
- // be stale, if e.g. the app was uninstalled while the activity was destroyed.
- super.onCreate(null);
-
- mFragmentManager = getSupportFragmentManager();
- mNotificationManager = getSystemService(NotificationManager.class);
-
- mUninstallRepository = new UninstallRepository(getApplicationContext());
- mUninstallViewModel = new ViewModelProvider(this,
- new UninstallViewModelFactory(this.getApplication(), mUninstallRepository)).get(
- UninstallViewModel.class);
-
- Intent intent = getIntent();
- CallerInfo callerInfo = new CallerInfo(
- intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME),
- intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
- mUninstallViewModel.preprocessIntent(intent, callerInfo);
-
- mUninstallViewModel.getCurrentUninstallStage().observe(this,
- this::onUninstallStageChange);
- }
-
- /**
- * Main controller of the UI. This method shows relevant dialogs / fragments based on the
- * uninstall stage
- */
- private void onUninstallStageChange(UninstallStage uninstallStage) {
- if (uninstallStage.getStageCode() == UninstallStage.STAGE_ABORTED) {
- UninstallAborted aborted = (UninstallAborted) uninstallStage;
- if (aborted.getAbortReason() == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE ||
- aborted.getAbortReason() == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) {
- UninstallErrorFragment errorDialog = new UninstallErrorFragment(aborted);
- showDialogInner(errorDialog);
- } else {
- setResult(aborted.getActivityResultCode(), null, true);
- }
- } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_USER_ACTION_REQUIRED) {
- UninstallUserActionRequired uar = (UninstallUserActionRequired) uninstallStage;
- UninstallConfirmationFragment confirmationDialog = new UninstallConfirmationFragment(
- uar);
- showDialogInner(confirmationDialog);
- } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_UNINSTALLING) {
- // TODO: This shows a fragment whether or not user requests a result or not.
- // Originally, if the user does not request a result, we used to show a notification.
- // And a fragment if the user requests a result back. Should we consolidate and
- // show a fragment always?
- UninstallUninstalling uninstalling = (UninstallUninstalling) uninstallStage;
- UninstallUninstallingFragment uninstallingDialog = new UninstallUninstallingFragment(
- uninstalling);
- showDialogInner(uninstallingDialog);
- } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_FAILED) {
- UninstallFailed failed = (UninstallFailed) uninstallStage;
- if (!failed.returnResult()) {
- mNotificationManager.notify(failed.getUninstallId(),
- failed.getUninstallNotification());
- }
- setResult(failed.getActivityResultCode(), failed.getResultIntent(), true);
- } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_SUCCESS) {
- UninstallSuccess success = (UninstallSuccess) uninstallStage;
- if (success.getMessage() != null) {
- Toast.makeText(this, success.getMessage(), Toast.LENGTH_LONG).show();
- }
- setResult(success.getActivityResultCode(), success.getResultIntent(), true);
- } else {
- Log.e(TAG, "Invalid stage: " + uninstallStage.getStageCode());
- showDialogInner(null);
- }
- }
-
- /**
- * Replace any visible dialog by the dialog returned by InstallRepository
- *
- * @param newDialog The new dialog to display
- */
- private void showDialogInner(DialogFragment newDialog) {
- DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
- TAG_DIALOG);
- if (currentDialog != null) {
- currentDialog.dismissAllowingStateLoss();
- }
- if (newDialog != null) {
- newDialog.show(mFragmentManager, TAG_DIALOG);
- }
- }
-
- public void setResult(int resultCode, Intent data, boolean shouldFinish) {
- super.setResult(resultCode, data);
- if (shouldFinish) {
- finish();
- }
- }
-
- @Override
- public void onPositiveResponse(boolean keepData) {
- mUninstallViewModel.initiateUninstall(keepData);
- }
-
- @Override
- public void onNegativeResponse() {
- mUninstallViewModel.cancelInstall();
- setResult(Activity.RESULT_FIRST_USER, null, true);
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt
new file mode 100644
index 0000000..0050c7e
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ * https://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.packageinstaller.v2.ui
+
+import android.app.Activity
+import android.app.NotificationManager
+import android.content.Intent
+import android.os.Bundle
+import android.os.Process
+import android.util.Log
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.ViewModelProvider
+import com.android.packageinstaller.v2.model.UninstallAborted
+import com.android.packageinstaller.v2.model.UninstallFailed
+import com.android.packageinstaller.v2.model.UninstallRepository
+import com.android.packageinstaller.v2.model.UninstallStage
+import com.android.packageinstaller.v2.model.UninstallSuccess
+import com.android.packageinstaller.v2.model.UninstallUninstalling
+import com.android.packageinstaller.v2.model.UninstallUserActionRequired
+import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment
+import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment
+import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment
+import com.android.packageinstaller.v2.viewmodel.UninstallViewModel
+import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory
+
+class UninstallLaunch : FragmentActivity(), UninstallActionListener {
+
+ companion object {
+ @JvmField val EXTRA_CALLING_PKG_UID =
+ UninstallLaunch::class.java.packageName + ".callingPkgUid"
+ @JvmField val EXTRA_CALLING_ACTIVITY_NAME =
+ UninstallLaunch::class.java.packageName + ".callingActivityName"
+ val LOG_TAG = UninstallLaunch::class.java.simpleName
+ private const val TAG_DIALOG = "dialog"
+ }
+
+ private var uninstallViewModel: UninstallViewModel? = null
+ private var uninstallRepository: UninstallRepository? = null
+ private var fragmentManager: FragmentManager? = null
+ private var notificationManager: NotificationManager? = null
+ override fun onCreate(savedInstanceState: Bundle?) {
+ window.addSystemFlags(WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS)
+
+ // Never restore any state, esp. never create any fragments. The data in the fragment might
+ // be stale, if e.g. the app was uninstalled while the activity was destroyed.
+ super.onCreate(null)
+ fragmentManager = supportFragmentManager
+ notificationManager = getSystemService(NotificationManager::class.java)
+
+ uninstallRepository = UninstallRepository(applicationContext)
+ uninstallViewModel = ViewModelProvider(
+ this, UninstallViewModelFactory(this.application, uninstallRepository!!)
+ ).get(UninstallViewModel::class.java)
+
+ val intent = intent
+ val callerInfo = UninstallRepository.CallerInfo(
+ intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME),
+ intent.getIntExtra(EXTRA_CALLING_PKG_UID, Process.INVALID_UID)
+ )
+ uninstallViewModel!!.preprocessIntent(intent, callerInfo)
+ uninstallViewModel!!.currentUninstallStage.observe(this) { uninstallStage: UninstallStage ->
+ onUninstallStageChange(uninstallStage)
+ }
+ }
+
+ /**
+ * Main controller of the UI. This method shows relevant dialogs / fragments based on the
+ * uninstall stage
+ */
+ private fun onUninstallStageChange(uninstallStage: UninstallStage) {
+ when (uninstallStage.stageCode) {
+ UninstallStage.STAGE_ABORTED -> {
+ val aborted = uninstallStage as UninstallAborted
+ if (aborted.abortReason == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE ||
+ aborted.abortReason == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED
+ ) {
+ val errorDialog = UninstallErrorFragment(aborted)
+ showDialogInner(errorDialog)
+ } else {
+ setResult(aborted.activityResultCode, null, true)
+ }
+ }
+
+ UninstallStage.STAGE_USER_ACTION_REQUIRED -> {
+ val uar = uninstallStage as UninstallUserActionRequired
+ val confirmationDialog = UninstallConfirmationFragment(uar)
+ showDialogInner(confirmationDialog)
+ }
+
+ UninstallStage.STAGE_UNINSTALLING -> {
+ // TODO: This shows a fragment whether or not user requests a result or not.
+ // Originally, if the user does not request a result, we used to show a notification.
+ // And a fragment if the user requests a result back. Should we consolidate and
+ // show a fragment always?
+ val uninstalling = uninstallStage as UninstallUninstalling
+ val uninstallingDialog = UninstallUninstallingFragment(uninstalling)
+ showDialogInner(uninstallingDialog)
+ }
+
+ UninstallStage.STAGE_FAILED -> {
+ val failed = uninstallStage as UninstallFailed
+ if (!failed.returnResult) {
+ notificationManager!!.notify(
+ failed.uninstallNotificationId!!, failed.uninstallNotification
+ )
+ }
+ setResult(failed.activityResultCode, failed.resultIntent, true)
+ }
+
+ UninstallStage.STAGE_SUCCESS -> {
+ val success = uninstallStage as UninstallSuccess
+ if (success.message != null) {
+ Toast.makeText(this, success.message, Toast.LENGTH_LONG).show()
+ }
+ setResult(success.activityResultCode, success.resultIntent, true)
+ }
+
+ else -> {
+ Log.e(LOG_TAG, "Invalid stage: " + uninstallStage.stageCode)
+ showDialogInner(null)
+ }
+ }
+ }
+
+ /**
+ * Replace any visible dialog by the dialog returned by InstallRepository
+ *
+ * @param newDialog The new dialog to display
+ */
+ private fun showDialogInner(newDialog: DialogFragment?) {
+ val currentDialog = fragmentManager!!.findFragmentByTag(TAG_DIALOG) as DialogFragment?
+ currentDialog?.dismissAllowingStateLoss()
+ newDialog?.show(fragmentManager!!, TAG_DIALOG)
+ }
+
+ fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
+ super.setResult(resultCode, data)
+ if (shouldFinish) {
+ finish()
+ }
+ }
+
+ override fun onPositiveResponse(keepData: Boolean) {
+ uninstallViewModel!!.initiateUninstall(keepData)
+ }
+
+ override fun onNegativeResponse() {
+ uninstallViewModel!!.cancelInstall()
+ setResult(Activity.RESULT_FIRST_USER, null, true)
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
index 6d6fcc9..679f696 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
@@ -24,8 +24,8 @@
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
-import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.model.InstallStage;
+import com.android.packageinstaller.v2.model.InstallUserActionRequired;
import com.android.packageinstaller.v2.ui.InstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
index 4cdce52..49901de 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
@@ -25,7 +25,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.model.InstallUserActionRequired;
import com.android.packageinstaller.v2.ui.InstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
index 6398aef..25363d0 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
@@ -28,7 +28,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.model.InstallUserActionRequired;
import com.android.packageinstaller.v2.ui.InstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java
index d45cd76..4667a7a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java
@@ -28,7 +28,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
+import com.android.packageinstaller.v2.model.InstallFailed;
import com.android.packageinstaller.v2.ui.InstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java
index 9f60f96..7327b5d 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java
@@ -25,7 +25,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
+import com.android.packageinstaller.v2.model.InstallInstalling;
/**
* Dialog to show when an install is in progress.
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
index ab6a932..b2a65faa 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
@@ -29,8 +29,8 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
-import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
+import com.android.packageinstaller.v2.model.InstallStage;
+import com.android.packageinstaller.v2.model.InstallSuccess;
import com.android.packageinstaller.v2.ui.InstallActionListener;
import java.util.List;
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
index 47fd67f..58b8b2d 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
@@ -24,7 +24,7 @@
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.InstallStage;
import com.android.packageinstaller.v2.ui.InstallActionListener;
public class SimpleErrorFragment extends DialogFragment {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
index 1b0885e..32ac4a6 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
@@ -30,7 +30,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import com.android.packageinstaller.v2.model.UninstallUserActionRequired;
import com.android.packageinstaller.v2.ui.UninstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
index 305daba..eb7183d 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
@@ -25,7 +25,7 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.model.UninstallAborted;
import com.android.packageinstaller.v2.ui.UninstallActionListener;
/**
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
index 23cc421..835efc6 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
@@ -22,7 +22,7 @@
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+import com.android.packageinstaller.v2.model.UninstallUninstalling;
/**
* Dialog to show that the app is uninstalling.
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
deleted file mode 100644
index 04a0622..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.viewmodel;
-
-import android.app.Application;
-import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.MutableLiveData;
-import com.android.packageinstaller.v2.model.InstallRepository;
-import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
-import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
-
-
-public class InstallViewModel extends AndroidViewModel {
-
- private static final String TAG = InstallViewModel.class.getSimpleName();
- private final InstallRepository mRepository;
- private final MediatorLiveData<InstallStage> mCurrentInstallStage = new MediatorLiveData<>(
- new InstallStaging());
-
- public InstallViewModel(@NonNull Application application, InstallRepository repository) {
- super(application);
- mRepository = repository;
- }
-
- public MutableLiveData<InstallStage> getCurrentInstallStage() {
- return mCurrentInstallStage;
- }
-
- public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
- InstallStage stage = mRepository.performPreInstallChecks(intent, callerInfo);
- if (stage.getStageCode() == InstallStage.STAGE_ABORTED) {
- mCurrentInstallStage.setValue(stage);
- } else {
- // Since staging is an async operation, we will get the staging result later in time.
- // Result of the file staging will be set in InstallRepository#mStagingResult.
- // As such, mCurrentInstallStage will need to add another MutableLiveData
- // as a data source
- mRepository.stageForInstall();
- mCurrentInstallStage.addSource(mRepository.getStagingResult(), installStage -> {
- if (installStage.getStageCode() != InstallStage.STAGE_READY) {
- mCurrentInstallStage.setValue(installStage);
- } else {
- checkIfAllowedAndInitiateInstall();
- }
- });
- }
- }
-
- public MutableLiveData<Integer> getStagingProgress() {
- return mRepository.getStagingProgress();
- }
-
- private void checkIfAllowedAndInitiateInstall() {
- InstallStage stage = mRepository.requestUserConfirmation();
- mCurrentInstallStage.setValue(stage);
- }
-
- public void forcedSkipSourceCheck() {
- InstallStage stage = mRepository.forcedSkipSourceCheck();
- mCurrentInstallStage.setValue(stage);
- }
-
- public void cleanupInstall() {
- mRepository.cleanupInstall();
- }
-
- public void reattemptInstall() {
- InstallStage stage = mRepository.reattemptInstall();
- mCurrentInstallStage.setValue(stage);
- }
-
- public void initiateInstall() {
- // Since installing is an async operation, we will get the install result later in time.
- // Result of the installation will be set in InstallRepository#mInstallResult.
- // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source
- mRepository.initiateInstall();
- mCurrentInstallStage.addSource(mRepository.getInstallResult(), installStage -> {
- if (installStage != null) {
- mCurrentInstallStage.setValue(installStage);
- }
- });
- }
-
- public int getStagedSessionId() {
- return mRepository.getStagedSessionId();
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
new file mode 100644
index 0000000..072fb2d
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.viewmodel
+
+import android.app.Application
+import android.content.Intent
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.packageinstaller.v2.model.InstallRepository
+import com.android.packageinstaller.v2.model.InstallStage
+import com.android.packageinstaller.v2.model.InstallStaging
+
+class InstallViewModel(application: Application, val repository: InstallRepository) :
+ AndroidViewModel(application) {
+
+ companion object {
+ private val LOG_TAG = InstallViewModel::class.java.simpleName
+ }
+
+ private val _currentInstallStage = MediatorLiveData<InstallStage>(InstallStaging())
+ val currentInstallStage: MutableLiveData<InstallStage>
+ get() = _currentInstallStage
+
+ fun preprocessIntent(intent: Intent, callerInfo: InstallRepository.CallerInfo) {
+ val stage = repository.performPreInstallChecks(intent, callerInfo)
+ if (stage.stageCode == InstallStage.STAGE_ABORTED) {
+ _currentInstallStage.value = stage
+ } else {
+ // Since staging is an async operation, we will get the staging result later in time.
+ // Result of the file staging will be set in InstallRepository#mStagingResult.
+ // As such, mCurrentInstallStage will need to add another MutableLiveData
+ // as a data source
+ repository.stageForInstall()
+ _currentInstallStage.addSource(repository.stagingResult) { installStage: InstallStage ->
+ if (installStage.stageCode != InstallStage.STAGE_READY) {
+ _currentInstallStage.value = installStage
+ } else {
+ checkIfAllowedAndInitiateInstall()
+ }
+ }
+ }
+ }
+
+ val stagingProgress: LiveData<Int>
+ get() = repository.stagingProgress
+
+ private fun checkIfAllowedAndInitiateInstall() {
+ val stage = repository.requestUserConfirmation()
+ _currentInstallStage.value = stage
+ }
+
+ fun forcedSkipSourceCheck() {
+ val stage = repository.forcedSkipSourceCheck()
+ _currentInstallStage.value = stage
+ }
+
+ fun cleanupInstall() {
+ repository.cleanupInstall()
+ }
+
+ fun reattemptInstall() {
+ val stage = repository.reattemptInstall()
+ _currentInstallStage.value = stage
+ }
+
+ fun initiateInstall() {
+ // Since installing is an async operation, we will get the install result later in time.
+ // Result of the installation will be set in InstallRepository#mInstallResult.
+ // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source
+ repository.initiateInstall()
+ _currentInstallStage.addSource(repository.installResult) { installStage: InstallStage? ->
+ if (installStage != null) {
+ _currentInstallStage.value = installStage
+ }
+ }
+ }
+
+ val stagedSessionId: Int
+ get() = repository.stagedSessionId
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
deleted file mode 100644
index ef459e6..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.viewmodel;
-
-import android.app.Application;
-import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-import com.android.packageinstaller.v2.model.InstallRepository;
-
-public class InstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory {
-
- private final InstallRepository mRepository;
- private final Application mApplication;
-
- public InstallViewModelFactory(Application application, InstallRepository repository) {
- // Calling super class' ctor ensures that create method is called correctly and the right
- // ctor of InstallViewModel is used. If we fail to do that, the default ctor:
- // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel
- super(application);
- mApplication = application;
- mRepository = repository;
- }
-
- @NonNull
- @Override
- @SuppressWarnings("unchecked")
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- return (T) new InstallViewModel(mApplication, mRepository);
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt
new file mode 100644
index 0000000..07b2f4f
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModelFactory.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.packageinstaller.v2.model.InstallRepository
+
+class InstallViewModelFactory(val application: Application, val repository: InstallRepository) :
+ ViewModelProvider.AndroidViewModelFactory(application) {
+
+ // Calling super class' ctor ensures that create method is called correctly and the right
+ // ctor of InstallViewModel is used. If we fail to do that, the default ctor:
+ // InstallViewModel(application) is used, and repository isn't initialized in the viewmodel
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return InstallViewModel(application, repository) as T
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
deleted file mode 100644
index 3f7bce8..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2023 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
- *
- * https://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.packageinstaller.v2.viewmodel;
-
-import android.app.Application;
-import android.content.Intent;
-import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.MediatorLiveData;
-import androidx.lifecycle.MutableLiveData;
-import com.android.packageinstaller.v2.model.UninstallRepository;
-import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
-import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
-
-public class UninstallViewModel extends AndroidViewModel {
-
- private static final String TAG = UninstallViewModel.class.getSimpleName();
- private final UninstallRepository mRepository;
- private final MediatorLiveData<UninstallStage> mCurrentUninstallStage =
- new MediatorLiveData<>();
-
- public UninstallViewModel(@NonNull Application application, UninstallRepository repository) {
- super(application);
- mRepository = repository;
- }
-
- public MutableLiveData<UninstallStage> getCurrentUninstallStage() {
- return mCurrentUninstallStage;
- }
-
- public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
- UninstallStage stage = mRepository.performPreUninstallChecks(intent, callerInfo);
- if (stage.getStageCode() != UninstallStage.STAGE_ABORTED) {
- stage = mRepository.generateUninstallDetails();
- }
- mCurrentUninstallStage.setValue(stage);
- }
-
- public void initiateUninstall(boolean keepData) {
- mRepository.initiateUninstall(keepData);
- // Since uninstall is an async operation, we will get the uninstall result later in time.
- // Result of the uninstall will be set in UninstallRepository#mUninstallResult.
- // As such, mCurrentUninstallStage will need to add another MutableLiveData
- // as a data source
- mCurrentUninstallStage.addSource(mRepository.getUninstallResult(), uninstallStage -> {
- if (uninstallStage != null) {
- mCurrentUninstallStage.setValue(uninstallStage);
- }
- });
- }
-
- public void cancelInstall() {
- mRepository.cancelInstall();
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt
new file mode 100644
index 0000000..80886e9
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ * https://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.packageinstaller.v2.viewmodel
+
+import android.app.Application
+import android.content.Intent
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import com.android.packageinstaller.v2.model.UninstallRepository
+import com.android.packageinstaller.v2.model.UninstallStage
+
+class UninstallViewModel(application: Application, val repository: UninstallRepository) :
+ AndroidViewModel(application) {
+
+ companion object {
+ private val LOG_TAG = UninstallViewModel::class.java.simpleName
+ }
+
+ private val _currentUninstallStage = MediatorLiveData<UninstallStage>()
+ val currentUninstallStage: MutableLiveData<UninstallStage>
+ get() = _currentUninstallStage
+
+ fun preprocessIntent(intent: Intent, callerInfo: UninstallRepository.CallerInfo) {
+ var stage = repository.performPreUninstallChecks(intent, callerInfo)
+ if (stage.stageCode != UninstallStage.STAGE_ABORTED) {
+ stage = repository.generateUninstallDetails()
+ }
+ _currentUninstallStage.value = stage
+ }
+
+ fun initiateUninstall(keepData: Boolean) {
+ repository.initiateUninstall(keepData)
+ // Since uninstall is an async operation, we will get the uninstall result later in time.
+ // Result of the uninstall will be set in UninstallRepository#mUninstallResult.
+ // As such, _currentUninstallStage will need to add another MutableLiveData
+ // as a data source
+ _currentUninstallStage.addSource(repository.uninstallResult) { uninstallStage: UninstallStage? ->
+ if (uninstallStage != null) {
+ _currentUninstallStage.value = uninstallStage
+ }
+ }
+ }
+
+ fun cancelInstall() {
+ repository.cancelInstall()
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java
deleted file mode 100644
index cd9845e..0000000
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright (C) 2023 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.packageinstaller.v2.viewmodel;
-
-import android.app.Application;
-import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-import com.android.packageinstaller.v2.model.UninstallRepository;
-
-public class UninstallViewModelFactory extends ViewModelProvider.AndroidViewModelFactory {
-
- private final UninstallRepository mRepository;
- private final Application mApplication;
-
- public UninstallViewModelFactory(Application application, UninstallRepository repository) {
- // Calling super class' ctor ensures that create method is called correctly and the right
- // ctor of UninstallViewModel is used. If we fail to do that, the default ctor:
- // UninstallViewModel(application) is used, and repository isn't initialized in
- // the viewmodel
- super(application);
- mApplication = application;
- mRepository = repository;
- }
-
- @NonNull
- @Override
- @SuppressWarnings("unchecked")
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- return (T) new UninstallViewModel(mApplication, mRepository);
- }
-}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt
new file mode 100644
index 0000000..0a316e7
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModelFactory.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.packageinstaller.v2.viewmodel
+
+import android.app.Application
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.packageinstaller.v2.model.UninstallRepository
+
+class UninstallViewModelFactory(val application: Application, val repository: UninstallRepository) :
+ ViewModelProvider.AndroidViewModelFactory(application) {
+
+ // Calling super class' ctor ensures that create method is called correctly and the right
+ // ctor of UninstallViewModel is used. If we fail to do that, the default ctor:
+ // UninstallViewModel(application) is used, and repository isn't initialized in
+ // the viewmodel
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ return UninstallViewModel(application, repository) as T
+ }
+}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index e5dbe5f..d6e8d26 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -112,5 +112,7 @@
Settings.Global.Wearable.SCREENSHOT_ENABLED,
Settings.Global.Wearable.SCREEN_UNLOCK_SOUND_ENABLED,
Settings.Global.Wearable.CHARGING_SOUNDS_ENABLED,
+ Settings.Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED,
+ Settings.Global.FORCE_ENABLE_PSS_PROFILING,
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 3027c5f..f8bdcf6 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -447,5 +447,7 @@
VALIDATORS.put(Global.Wearable.WEAR_LAUNCHER_UI_MODE, ANY_INTEGER_VALIDATOR);
VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Global.Wearable.WRIST_DETECTION_AUTO_LOCKING_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Global.FORCE_ENABLE_PSS_PROFILING, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SystemUI/res/drawable/connected_display_dialog_bg.xml b/packages/SystemUI/res/drawable/connected_display_dialog_bg.xml
new file mode 100644
index 0000000..2dce37d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/connected_display_dialog_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:topLeftRadius="28dp"
+ android:topRightRadius="28dp"/>
+ <solid android:color="?android:attr/colorBackground" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/connected_display_dialog.xml b/packages/SystemUI/res/layout/connected_display_dialog.xml
index 8d7f7eb..a71782b 100644
--- a/packages/SystemUI/res/layout/connected_display_dialog.xml
+++ b/packages/SystemUI/res/layout/connected_display_dialog.xml
@@ -22,7 +22,7 @@
android:orientation="vertical"
android:paddingHorizontal="@dimen/dialog_side_padding"
android:paddingTop="@dimen/dialog_top_padding"
- android:background="@*android:drawable/bottomsheet_background"
+ android:background="@drawable/connected_display_dialog_bg"
android:paddingBottom="@dimen/dialog_bottom_padding">
<ImageView
@@ -40,7 +40,7 @@
android:id="@+id/connected_display_dialog_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/screenrecord_title_margin_top"
+ android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/connected_display_dialog_start_mirroring"
android:textAppearance="@style/TextAppearance.Dialog.Title" />
@@ -51,13 +51,14 @@
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone"
+ android:layout_marginTop="16dp"
android:text="@string/connected_display_dialog_dual_display_stop_warning"
android:textAppearance="@style/TextAppearance.Dialog.Body" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/screenrecord_buttons_margin_top"
+ android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f4b25a7..e7eb984 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3265,7 +3265,7 @@
<!--- Title of the dialog appearing when an external display is connected, asking whether to start mirroring [CHAR LIMIT=NONE]-->
<string name="connected_display_dialog_start_mirroring">Mirror to external display?</string>
<!--- Body of the mirroring dialog, shown when dual display is enabled. This signals that enabling mirroring will stop concurrent displays on a foldable device. [CHAR LIMIT=NONE]-->
- <string name="connected_display_dialog_dual_display_stop_warning">Any dual screen activity currently running will be stopped</string>
+ <string name="connected_display_dialog_dual_display_stop_warning">Your inner display will be mirrored. Your front display will be turned off.</string>
<!--- Label of the "enable display" button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]-->
<string name="mirror_display">Mirror display</string>
<!--- Label of the dismiss button of the dialog appearing when an external display is connected [CHAR LIMIT=NONE]-->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
index 92f66902..387f2e1 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginInstance.java
@@ -101,7 +101,7 @@
}
/** Alerts listener and plugin that the plugin has been created. */
- public void onCreate() {
+ public synchronized void onCreate() {
boolean loadPlugin = mListener.onPluginAttached(this);
if (!loadPlugin) {
if (mPlugin != null) {
@@ -128,7 +128,7 @@
}
/** Alerts listener and plugin that the plugin is being shutdown. */
- public void onDestroy() {
+ public synchronized void onDestroy() {
logDebug("onDestroy");
unloadPlugin();
mListener.onPluginDetached(this);
@@ -143,12 +143,13 @@
/**
* Loads and creates the plugin if it does not exist.
*/
- public void loadPlugin() {
+ public synchronized void loadPlugin() {
if (mPlugin != null) {
logDebug("Load request when already loaded");
return;
}
+ // Both of these calls take about 1 - 1.5 seconds in test runs
mPlugin = mPluginFactory.createPlugin();
mPluginContext = mPluginFactory.createPluginContext();
if (mPlugin == null || mPluginContext == null) {
@@ -171,7 +172,7 @@
*
* This will free the associated memory if there are not other references.
*/
- public void unloadPlugin() {
+ public synchronized void unloadPlugin() {
if (mPlugin == null) {
logDebug("Unload request when already unloaded");
return;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
rename to packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
index b1de127..49e0df6 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java
@@ -31,7 +31,7 @@
* Controls the interaction between {@link MagnetizedObject} and
* {@link MagnetizedObject.MagneticTarget}.
*/
-class DismissAnimationController {
+class DragToInteractAnimationController {
private static final boolean ENABLE_FLING_TO_DISMISS_MENU = false;
private static final float COMPLETELY_OPAQUE = 1.0f;
private static final float COMPLETELY_TRANSPARENT = 0.0f;
@@ -45,7 +45,7 @@
private float mMinDismissSize;
private float mSizePercent;
- DismissAnimationController(DismissView dismissView, MenuView menuView) {
+ DragToInteractAnimationController(DismissView dismissView, MenuView menuView) {
mDismissView = dismissView;
mDismissView.setPivotX(dismissView.getWidth() / 2.0f);
mDismissView.setPivotY(dismissView.getHeight() / 2.0f);
@@ -127,7 +127,7 @@
* @param event that move the magnetized object which is also the menu list view.
* @return true if the location of the motion events moves within the magnetic field of a
* target, but false if didn't set
- * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+ * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
*/
boolean maybeConsumeMoveMotionEvent(MotionEvent event) {
return mMagnetizedObject.maybeConsumeMotionEvent(event);
@@ -140,7 +140,7 @@
* @param event that move the magnetized object which is also the menu list view.
* @return true if the location of the motion events moves within the magnetic field of a
* target, but false if didn't set
- * {@link DismissAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
+ * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}.
*/
boolean maybeConsumeUpMotionEvent(MotionEvent event) {
return mMagnetizedObject.maybeConsumeMotionEvent(event);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index 34d7cec..a270558 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -73,7 +73,7 @@
private final ValueAnimator mFadeOutAnimator;
private final Handler mHandler;
private boolean mIsFadeEffectEnabled;
- private DismissAnimationController.DismissCallback mDismissCallback;
+ private DragToInteractAnimationController.DismissCallback mDismissCallback;
private Runnable mSpringAnimationsEndAction;
// Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
@@ -171,7 +171,7 @@
}
void setDismissCallback(
- DismissAnimationController.DismissCallback dismissCallback) {
+ DragToInteractAnimationController.DismissCallback dismissCallback) {
mDismissCallback = dismissCallback;
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
index d01590f..52e7b91 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java
@@ -40,13 +40,13 @@
private final PointF mMenuTranslationDown = new PointF();
private boolean mIsDragging = false;
private float mTouchSlop;
- private final DismissAnimationController mDismissAnimationController;
+ private final DragToInteractAnimationController mDragToInteractAnimationController;
private Optional<Runnable> mOnActionDownEnd = Optional.empty();
MenuListViewTouchHandler(MenuAnimationController menuAnimationController,
- DismissAnimationController dismissAnimationController) {
+ DragToInteractAnimationController dragToInteractAnimationController) {
mMenuAnimationController = menuAnimationController;
- mDismissAnimationController = dismissAnimationController;
+ mDragToInteractAnimationController = dragToInteractAnimationController;
}
@Override
@@ -67,7 +67,7 @@
mMenuTranslationDown.set(menuView.getTranslationX(), menuView.getTranslationY());
mMenuAnimationController.cancelAnimations();
- mDismissAnimationController.maybeConsumeDownMotionEvent(motionEvent);
+ mDragToInteractAnimationController.maybeConsumeDownMotionEvent(motionEvent);
mOnActionDownEnd.ifPresent(Runnable::run);
break;
@@ -78,9 +78,10 @@
mMenuAnimationController.onDraggingStart();
}
- mDismissAnimationController.showDismissView(/* show= */ true);
+ mDragToInteractAnimationController.showDismissView(/* show= */ true);
- if (!mDismissAnimationController.maybeConsumeMoveMotionEvent(motionEvent)) {
+ if (!mDragToInteractAnimationController.maybeConsumeMoveMotionEvent(
+ motionEvent)) {
mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx);
mMenuAnimationController.moveToPositionYIfNeeded(
mMenuTranslationDown.y + dy);
@@ -94,17 +95,18 @@
mIsDragging = false;
if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) {
- mDismissAnimationController.showDismissView(/* show= */ false);
+ mDragToInteractAnimationController.showDismissView(/* show= */ false);
mMenuAnimationController.fadeOutIfEnabled();
return true;
}
- if (!mDismissAnimationController.maybeConsumeUpMotionEvent(motionEvent)) {
+ if (!mDragToInteractAnimationController.maybeConsumeUpMotionEvent(
+ motionEvent)) {
mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS);
mMenuAnimationController.flingMenuThenSpringToEdge(endX,
mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
- mDismissAnimationController.showDismissView(/* show= */ false);
+ mDragToInteractAnimationController.showDismissView(/* show= */ false);
}
// Avoid triggering the listener of the item.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index ff3a9e3..62d5feb 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -94,7 +94,7 @@
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final IAccessibilityFloatingMenu mFloatingMenu;
private final SecureSettings mSecureSettings;
- private final DismissAnimationController mDismissAnimationController;
+ private final DragToInteractAnimationController mDragToInteractAnimationController;
private final MenuViewModel mMenuViewModel;
private final Observer<Boolean> mDockTooltipObserver =
this::onDockTooltipVisibilityChanged;
@@ -188,29 +188,30 @@
mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
mDismissView = new DismissView(context);
DismissViewUtils.setup(mDismissView);
- mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
- mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
+ mDragToInteractAnimationController = new DragToInteractAnimationController(
+ mDismissView, mMenuView);
+ mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
- mDismissAnimationController.animateDismissMenu(/* scaleUp= */ true);
+ mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ true);
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
float velocityX, float velocityY, boolean wasFlungOut) {
- mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+ mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false);
}
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
hideMenuAndShowMessage();
mDismissView.hide();
- mDismissAnimationController.animateDismissMenu(/* scaleUp= */ false);
+ mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false);
}
});
mMenuListViewTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
- mDismissAnimationController);
+ mDragToInteractAnimationController);
mMenuView.addOnItemTouchListenerToList(mMenuListViewTouchHandler);
mMenuView.setMoveToTuckedListener(this);
@@ -243,7 +244,7 @@
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
mDismissView.updateResources();
- mDismissAnimationController.updateResources();
+ mDragToInteractAnimationController.updateResources();
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
index 24240df..940d1e1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt
@@ -142,6 +142,11 @@
return true
}
+ if (renderer == null || onDestroy == null) {
+ Log.wtf(TAG, "Renderer/onDestroy should not be null.")
+ return true
+ }
+
when (message.what) {
KeyguardPreviewConstants.MESSAGE_ID_SLOT_SELECTED -> {
message.data.getString(KeyguardPreviewConstants.KEY_SLOT_ID)?.let { slotId ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
similarity index 84%
rename from packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
index fd258e3..9bcab57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java
@@ -40,12 +40,12 @@
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-/** Tests for {@link DismissAnimationController}. */
+/** Tests for {@link DragToInteractAnimationController}. */
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
-public class DismissAnimationControllerTest extends SysuiTestCase {
- private DismissAnimationController mDismissAnimationController;
+public class DragToInteractAnimationControllerTest extends SysuiTestCase {
+ private DragToInteractAnimationController mDragToInteractAnimationController;
private DismissView mDismissView;
@Rule
@@ -65,19 +65,20 @@
stubMenuViewAppearance);
mDismissView = spy(new DismissView(mContext));
DismissViewUtils.setup(mDismissView);
- mDismissAnimationController = new DismissAnimationController(mDismissView, stubMenuView);
+ mDragToInteractAnimationController = new DragToInteractAnimationController(
+ mDismissView, stubMenuView);
}
@Test
public void showDismissView_success() {
- mDismissAnimationController.showDismissView(true);
+ mDragToInteractAnimationController.showDismissView(true);
verify(mDismissView).show();
}
@Test
public void hideDismissView_success() {
- mDismissAnimationController.showDismissView(false);
+ mDragToInteractAnimationController.showDismissView(false);
verify(mDismissView).hide();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
index 7f12c05..9c8de30 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java
@@ -62,7 +62,7 @@
@Mock
private SecureSettings mSecureSettings;
@Mock
- private DismissAnimationController.DismissCallback mStubDismissCallback;
+ private DragToInteractAnimationController.DismissCallback mStubDismissCallback;
private RecyclerView mStubListView;
private MenuView mMenuView;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
index 9797f2a..e1522f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java
@@ -68,7 +68,7 @@
private MenuView mStubMenuView;
private MenuListViewTouchHandler mTouchHandler;
private MenuAnimationController mMenuAnimationController;
- private DismissAnimationController mDismissAnimationController;
+ private DragToInteractAnimationController mDragToInteractAnimationController;
private RecyclerView mStubListView;
private DismissView mDismissView;
@@ -92,10 +92,10 @@
mStubMenuView, stubMenuViewAppearance));
mDismissView = spy(new DismissView(mContext));
DismissViewUtils.setup(mDismissView);
- mDismissAnimationController =
- spy(new DismissAnimationController(mDismissView, mStubMenuView));
+ mDragToInteractAnimationController =
+ spy(new DragToInteractAnimationController(mDismissView, mStubMenuView));
mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController,
- mDismissAnimationController);
+ mDragToInteractAnimationController);
final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets);
mStubListView = (RecyclerView) mStubMenuView.getChildAt(0);
mStubListView.setAdapter(stubAdapter);
@@ -115,7 +115,7 @@
@Test
public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() {
- doReturn(false).when(mDismissAnimationController).maybeConsumeMoveMotionEvent(
+ doReturn(false).when(mDragToInteractAnimationController).maybeConsumeMoveMotionEvent(
any(MotionEvent.class));
final int offset = 100;
final MotionEvent stubDownEvent =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
index 6eabf44..5e57c83 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/plugins/PluginInstanceTest.java
@@ -17,13 +17,17 @@
package com.android.systemui.shared.plugins;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
import androidx.test.runner.AndroidJUnit4;
@@ -40,7 +44,11 @@
import java.lang.ref.WeakReference;
import java.util.Collections;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -104,6 +112,7 @@
mPluginInstance = mPluginInstanceFactory.create(
mContext, mAppInfo, TEST_PLUGIN_COMPONENT_NAME,
TestPlugin.class, mPluginListener);
+ mPluginInstance.setIsDebug(true);
mPluginContext = new WeakReference<>(mPluginInstance.getPluginContext());
}
@@ -158,7 +167,7 @@
@Test
public void testOnAttach_SkipLoad() {
- mPluginListener.mAttachReturn = false;
+ mPluginListener.mOnAttach = () -> false;
mPluginInstance.onCreate();
assertEquals(1, mPluginListener.mAttachedCount);
assertEquals(0, mPluginListener.mLoadCount);
@@ -166,6 +175,65 @@
assertInstances(0, 0);
}
+ @Test
+ public void testLoadUnloadSimultaneous_HoldsUnload() throws Exception {
+ final Semaphore loadLock = new Semaphore(1);
+ final Semaphore unloadLock = new Semaphore(1);
+
+ mPluginListener.mOnAttach = () -> false;
+ mPluginListener.mOnLoad = () -> {
+ assertNotNull(mPluginInstance.getPlugin());
+
+ // Allow the bg thread the opportunity to delete the plugin
+ loadLock.release();
+ Thread.yield();
+ boolean isLocked = getLock(unloadLock, 1000);
+
+ // Ensure the bg thread failed to do delete the plugin
+ assertNotNull(mPluginInstance.getPlugin());
+ // We expect that bgThread deadlocked holding the semaphore
+ assertFalse(isLocked);
+ };
+
+ AtomicBoolean isBgThreadFailed = new AtomicBoolean(false);
+ Thread bgThread = new Thread(() -> {
+ assertTrue(getLock(unloadLock, 10));
+ assertTrue(getLock(loadLock, 3000)); // Wait for the foreground thread
+ assertNotNull(mPluginInstance.getPlugin());
+ // Attempt to delete the plugin, this should block until the load completes
+ mPluginInstance.unloadPlugin();
+ assertNull(mPluginInstance.getPlugin());
+ unloadLock.release();
+ loadLock.release();
+ });
+
+ // This protects the test suite from crashing due to the uncaught exception.
+ bgThread.setUncaughtExceptionHandler((Thread t, Throwable ex) -> {
+ Log.e("testLoadUnloadSimultaneous_HoldsUnload", "Exception from BG Thread", ex);
+ isBgThreadFailed.set(true);
+ });
+
+ loadLock.acquire();
+ mPluginInstance.onCreate();
+
+ assertNull(mPluginInstance.getPlugin());
+ bgThread.start();
+ mPluginInstance.loadPlugin();
+
+ bgThread.join(5000);
+ assertFalse(isBgThreadFailed.get());
+ assertNull(mPluginInstance.getPlugin());
+ }
+
+ private boolean getLock(Semaphore lock, long millis) {
+ try {
+ return lock.tryAcquire(millis, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ex) {
+ fail();
+ return false;
+ }
+ }
+
// This target class doesn't matter, it just needs to have a Requires to hit the flow where
// the mock version info is called.
@ProvidesInterface(action = TestPlugin.ACTION, version = TestPlugin.VERSION)
@@ -226,7 +294,10 @@
}
public class FakeListener implements PluginListener<TestPlugin> {
- public boolean mAttachReturn = true;
+ public Supplier<Boolean> mOnAttach = null;
+ public Runnable mOnDetach = null;
+ public Runnable mOnLoad = null;
+ public Runnable mOnUnload = null;
public int mAttachedCount = 0;
public int mDetachedCount = 0;
public int mLoadCount = 0;
@@ -236,13 +307,16 @@
public boolean onPluginAttached(PluginLifecycleManager<TestPlugin> manager) {
mAttachedCount++;
assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
- return mAttachReturn;
+ return mOnAttach != null ? mOnAttach.get() : true;
}
@Override
public void onPluginDetached(PluginLifecycleManager<TestPlugin> manager) {
mDetachedCount++;
assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
+ if (mOnDetach != null) {
+ mOnDetach.run();
+ }
}
@Override
@@ -261,6 +335,9 @@
assertEquals(expectedContext, pluginContext);
}
assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
+ if (mOnLoad != null) {
+ mOnLoad.run();
+ }
}
@Override
@@ -274,6 +351,9 @@
assertEquals(expectedPlugin, plugin);
}
assertEquals(PluginInstanceTest.this.mPluginInstance, manager);
+ if (mOnUnload != null) {
+ mOnUnload.run();
+ }
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 7558974..1236fcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -38,6 +38,7 @@
import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
import android.metrics.LogMaker;
+import android.platform.test.annotations.DisableFlags;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
@@ -84,6 +85,7 @@
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -218,6 +220,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_notificationsVisible_zenHiding() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(true);
initController(/* viewIsAttached= */ true);
@@ -238,6 +241,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_notificationsHidden_zenNotHiding() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -258,6 +262,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_splitShadeMode_alwaysShowEmptyView() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -285,6 +290,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_bouncerShowing_flagOff_hideEmptyView() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -306,6 +312,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_bouncerShowing_flagOn_hideEmptyView() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -327,6 +334,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_bouncerNotShowing_flagOff_showEmptyView() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -348,6 +356,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testUpdateEmptyShadeView_bouncerNotShowing_flagOn_showEmptyView() {
when(mZenModeController.areNotificationsHiddenInShade()).thenReturn(false);
initController(/* viewIsAttached= */ true);
@@ -504,6 +513,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testSetNotifStats_updatesHasFilteredOutSeenNotifications() {
initController(/* viewIsAttached= */ true);
mSeenNotificationsInteractor.setHasFilteredOutSeenNotifications(true);
@@ -545,6 +555,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateImportantForAccessibility_noChild_onKeyGuard_notImportantForA11y() {
// GIVEN: Controller is attached, active notifications is empty,
// and mNotificationStackScrollLayout.onKeyguard() is true
@@ -561,6 +572,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateImportantForAccessibility_hasChild_onKeyGuard_importantForA11y() {
// GIVEN: Controller is attached, active notifications is not empty,
// and mNotificationStackScrollLayout.onKeyguard() is true
@@ -584,6 +596,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateImportantForAccessibility_hasChild_notOnKeyGuard_importantForA11y() {
// GIVEN: Controller is attached, active notifications is not empty,
// and mNotificationStackScrollLayout.onKeyguard() is false
@@ -607,6 +620,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateImportantForAccessibility_noChild_notOnKeyGuard_importantForA11y() {
// GIVEN: Controller is attached, active notifications is empty,
// and mNotificationStackScrollLayout.onKeyguard() is false
@@ -623,6 +637,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateEmptyShadeView_onKeyguardTransitionToAod_hidesView() {
initController(/* viewIsAttached= */ true);
mController.onKeyguardTransitionChanged(
@@ -633,6 +648,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void updateEmptyShadeView_onKeyguardOccludedTransitionToAod_hidesView() {
initController(/* viewIsAttached= */ true);
mController.onKeyguardTransitionChanged(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index ad7dee3..83ba684 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -51,6 +51,8 @@
import android.graphics.Insets;
import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableResources;
@@ -81,6 +83,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
@@ -191,7 +194,7 @@
mStackScrollerInternal.initView(getContext(), mNotificationSwipeHelper,
mNotificationStackSizeCalculator);
mStackScroller = spy(mStackScrollerInternal);
- mStackScroller.setResetUserExpandedStatesRunnable(()->{});
+ mStackScroller.setResetUserExpandedStatesRunnable(() -> {});
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mStackScrollLayoutController.isHistoryEnabled()).thenReturn(true);
when(mStackScrollLayoutController.getNotificationRoundnessManager())
@@ -309,7 +312,9 @@
public void updateEmptyView_dndSuppressing() {
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, true);
+ mStackScroller.updateEmptyShadeView(/* visible = */ true,
+ /* areNotificationsHiddenInShade = */ true,
+ /* hasFilteredOutSeenNotifications = */ false);
verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
}
@@ -319,7 +324,9 @@
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, false);
+ mStackScroller.updateEmptyShadeView(/* visible = */ true,
+ /* areNotificationsHiddenInShade = */ false,
+ /* hasFilteredOutSeenNotifications = */ false);
verify(mEmptyShadeView).setText(R.string.empty_shade_text);
}
@@ -328,10 +335,14 @@
public void updateEmptyView_noNotificationsToDndSuppressing() {
mStackScroller.setEmptyShadeView(mEmptyShadeView);
when(mEmptyShadeView.willBeGone()).thenReturn(true);
- mStackScroller.updateEmptyShadeView(true, false);
+ mStackScroller.updateEmptyShadeView(/* visible = */ true,
+ /* areNotificationsHiddenInShade = */ false,
+ /* hasFilteredOutSeenNotifications = */ false);
verify(mEmptyShadeView).setText(R.string.empty_shade_text);
- mStackScroller.updateEmptyShadeView(true, true);
+ mStackScroller.updateEmptyShadeView(/* visible = */ true,
+ /* areNotificationsHiddenInShade = */ true,
+ /* hasFilteredOutSeenNotifications = */ false);
verify(mEmptyShadeView).setText(R.string.dnd_suppressing_shade_text);
}
@@ -385,8 +396,8 @@
mStackScroller.setExpandedHeight(100f);
}
-
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void manageNotifications_visible() {
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -399,6 +410,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void clearAll_visible() {
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
@@ -411,6 +423,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testInflateFooterView() {
mStackScroller.inflateFooterView();
ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class);
@@ -444,7 +457,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(false, true, true);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true);
}
@Test
@@ -459,7 +472,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(false, false, true);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(false, false, true);
}
@Test
@@ -474,7 +487,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(true, true, true);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, true);
}
@Test
@@ -490,7 +503,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(true, true, false);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(true, true, false);
}
@Test
@@ -505,7 +518,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(false, true, true);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(false, true, true);
}
@Test
@@ -521,7 +534,7 @@
FooterView view = mock(FooterView.class);
mStackScroller.setFooterView(view);
mStackScroller.updateFooter();
- verify(mStackScroller).updateFooterView(true, false, true);
+ verify(mStackScroller, atLeastOnce()).updateFooterView(true, false, true);
}
@Test
@@ -529,7 +542,8 @@
mStackScroller.setCurrentUserSetup(true);
// add footer
- mStackScroller.inflateFooterView();
+ FooterView view = mock(FooterView.class);
+ mStackScroller.setFooterView(view);
// add notification
ExpandableNotificationRow row = createClearableRow();
@@ -545,6 +559,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void testReInflatesFooterViews() {
when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text);
clearInvocations(mStackScroller);
@@ -554,6 +569,16 @@
}
@Test
+ @EnableFlags(FooterViewRefactor.FLAG_NAME)
+ public void testReInflatesEmptyShadeView() {
+ when(mEmptyShadeView.getTextResource()).thenReturn(R.string.empty_shade_text);
+ clearInvocations(mStackScroller);
+ mStackScroller.reinflateViews();
+ verify(mStackScroller, never()).setFooterView(any());
+ verify(mStackScroller).setEmptyShadeView(any());
+ }
+
+ @Test
public void testSetIsBeingDraggedResetsExposedMenu() {
mStackScroller.setIsBeingDragged(true);
verify(mNotificationSwipeHelper).resetExposedMenuView(true, true);
@@ -601,6 +626,8 @@
@Test
public void testClearNotifications_clearAllInProgress() {
+ mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
+
ExpandableNotificationRow row = createClearableRow();
when(row.getEntry().hasFinishedInitialization()).thenReturn(true);
doReturn(true).when(mStackScroller).isVisible(row);
@@ -645,6 +672,8 @@
@Test
public void testAddNotificationUpdatesSpeedBumpIndex() {
+ mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
+
// initial state calculated == 0
assertEquals(0, mStackScroller.getSpeedBumpIndex());
@@ -661,6 +690,8 @@
@Test
public void testAddAmbientNotificationNoSpeedBumpUpdate() {
+ mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
+
// initial state calculated == 0
assertEquals(0, mStackScroller.getSpeedBumpIndex());
@@ -677,6 +708,8 @@
@Test
public void testRemoveNotificationUpdatesSpeedBump() {
+ mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
+
// initial state calculated == 0
assertEquals(0, mStackScroller.getSpeedBumpIndex());
@@ -872,6 +905,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void hasFilteredOutSeenNotifs_updateFooter() {
mStackScroller.setCurrentUserSetup(true);
@@ -887,6 +921,7 @@
}
@Test
+ @DisableFlags(FooterViewRefactor.FLAG_NAME)
public void hasFilteredOutSeenNotifs_updateEmptyShadeView() {
mStackScroller.setHasFilteredOutSeenNotifications(true);
mStackScroller.updateEmptyShadeView(true, false);
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 1c3c21c..72e62c3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -650,6 +650,10 @@
// foreground service background start restriction.
volatile boolean mFgsStartRestrictionNotificationEnabled = false;
+ // Indicates whether PSS profiling in AppProfiler is force-enabled, even if RSS is used by
+ // default. Controlled by Settings.Global.FORCE_ENABLE_PSS_PROFILING
+ volatile boolean mForceEnablePssProfiling = false;
+
/**
* Indicates whether the foreground service background start restriction is enabled for
* caller app that is targeting S+.
@@ -979,6 +983,9 @@
private static final Uri ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS_URI =
Settings.Global.getUriFor(Settings.Global.ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS);
+ private static final Uri FORCE_ENABLE_PSS_PROFILING_URI =
+ Settings.Global.getUriFor(Settings.Global.FORCE_ENABLE_PSS_PROFILING);
+
/**
* The threshold to decide if a given association should be dumped into metrics.
*/
@@ -1389,6 +1396,7 @@
mResolver.registerContentObserver(ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS_URI,
false, this);
}
+ mResolver.registerContentObserver(FORCE_ENABLE_PSS_PROFILING_URI, false, this);
updateConstants();
if (mSystemServerAutomaticHeapDumpEnabled) {
updateEnableAutomaticSystemServerHeapDumps();
@@ -1404,6 +1412,7 @@
// The following read from Settings.
updateActivityStartsLoggingEnabled();
updateForegroundServiceStartsLoggingEnabled();
+ updateForceEnablePssProfiling();
// Read DropboxRateLimiter params from flags.
mService.initDropboxRateLimiter();
}
@@ -1445,6 +1454,8 @@
updateForegroundServiceStartsLoggingEnabled();
} else if (ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS_URI.equals(uri)) {
updateEnableAutomaticSystemServerHeapDumps();
+ } else if (FORCE_ENABLE_PSS_PROFILING_URI.equals(uri)) {
+ updateForceEnablePssProfiling();
}
}
@@ -1559,6 +1570,11 @@
Settings.Global.ACTIVITY_STARTS_LOGGING_ENABLED, 1) == 1;
}
+ private void updateForceEnablePssProfiling() {
+ mForceEnablePssProfiling = Settings.Global.getInt(mResolver,
+ Settings.Global.FORCE_ENABLE_PSS_PROFILING, 0) == 1;
+ }
+
private void updateBackgroundActivityStarts() {
mFlagBackgroundActivityStartsEnabled = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index f8451fd..671c8e9 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -327,7 +327,6 @@
import android.os.DropBoxManager;
import android.os.FactoryTest;
import android.os.FileUtils;
-import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.IDeviceIdentifiersPolicyService;
@@ -8608,7 +8607,7 @@
final long initialIdlePssOrRss, lastPssOrRss, lastSwapPss;
synchronized (mAppProfiler.mProfilerLock) {
initialIdlePssOrRss = pr.getInitialIdlePssOrRss();
- lastPssOrRss = !Flags.removeAppProfilerPssCollection()
+ lastPssOrRss = mAppProfiler.isProfilingPss()
? pr.getLastPss() : pr.getLastRss();
lastSwapPss = pr.getLastSwapPss();
}
@@ -8618,14 +8617,14 @@
final StringBuilder sb2 = new StringBuilder(128);
sb2.append("Kill");
sb2.append(proc.processName);
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (mAppProfiler.isProfilingPss()) {
sb2.append(" in idle maint: pss=");
} else {
sb2.append(" in idle maint: rss=");
}
sb2.append(lastPssOrRss);
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (mAppProfiler.isProfilingPss()) {
sb2.append(", swapPss=");
sb2.append(lastSwapPss);
sb2.append(", initialPss=");
@@ -8640,7 +8639,7 @@
Slog.wtfQuiet(TAG, sb2.toString());
mHandler.post(() -> {
synchronized (ActivityManagerService.this) {
- proc.killLocked(!Flags.removeAppProfilerPssCollection()
+ proc.killLocked(mAppProfiler.isProfilingPss()
? "idle maint (pss " : "idle maint (rss " + lastPssOrRss
+ " from " + initialIdlePssOrRss + ")",
ApplicationExitInfo.REASON_OTHER,
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 2e0aec9..e4956b3 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -602,7 +602,7 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case COLLECT_PSS_BG_MSG:
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (isProfilingPss()) {
collectPssInBackground();
} else {
collectRssInBackground();
@@ -748,6 +748,11 @@
} while (true);
}
+ boolean isProfilingPss() {
+ return !Flags.removeAppProfilerPssCollection()
+ || mService.mConstants.mForceEnablePssProfiling;
+ }
+
// This method is analogous to collectPssInBackground() and is intended to be used as a
// replacement if Flags.removeAppProfilerPssCollection() is enabled. References to PSS in
// methods outside of AppProfiler have generally been kept where a new RSS equivalent is not
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b507a60..f49e25a 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -143,7 +143,6 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.ServiceInfo;
import android.net.NetworkPolicyManager;
-import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManagerInternal;
@@ -2418,7 +2417,7 @@
// normally be a B service, but if we are low on RAM and it
// is large we want to force it down since we would prefer to
// keep launcher over it.
- long lastPssOrRss = !Flags.removeAppProfilerPssCollection()
+ long lastPssOrRss = mService.mAppProfiler.isProfilingPss()
? app.mProfile.getLastPss() : app.mProfile.getLastRss();
// RSS is larger than PSS, but the RSS/PSS ratio varies per-process based on how
@@ -2427,9 +2426,8 @@
//
// TODO(b/296454553): Tune the second value so that the relative number of
// service B is similar before/after this flag is enabled.
- double thresholdModifier = !Flags.removeAppProfilerPssCollection()
- ? 1
- : mConstants.PSS_TO_RSS_THRESHOLD_MODIFIER;
+ double thresholdModifier = mService.mAppProfiler.isProfilingPss()
+ ? 1 : mConstants.PSS_TO_RSS_THRESHOLD_MODIFIER;
double cachedRestoreThreshold =
mProcessList.getCachedRestoreThresholdKb() * thresholdModifier;
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 9a9d4ee..b03183c 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -94,7 +94,6 @@
import android.os.Build;
import android.os.Bundle;
import android.os.DropBoxManager;
-import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -4732,7 +4731,7 @@
pw.print("state: cur="); pw.print(makeProcStateString(state.getCurProcState()));
pw.print(" set="); pw.print(makeProcStateString(state.getSetProcState()));
// These values won't be collected if the flag is enabled.
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (service.mAppProfiler.isProfilingPss()) {
pw.print(" lastPss=");
DebugUtils.printSizeValue(pw, r.mProfile.getLastPss() * 1024);
pw.print(" lastSwapPss=");
diff --git a/services/core/java/com/android/server/am/ProcessProfileRecord.java b/services/core/java/com/android/server/am/ProcessProfileRecord.java
index 8ca64f8..d8f797c 100644
--- a/services/core/java/com/android/server/am/ProcessProfileRecord.java
+++ b/services/core/java/com/android/server/am/ProcessProfileRecord.java
@@ -23,7 +23,6 @@
import android.app.ProcessMemoryState.HostingComponentType;
import android.content.pm.ApplicationInfo;
import android.os.Debug;
-import android.os.Flags;
import android.os.Process;
import android.os.SystemClock;
import android.util.DebugUtils;
@@ -677,7 +676,7 @@
void dumpPss(PrintWriter pw, String prefix, long nowUptime) {
synchronized (mProfilerLock) {
// TODO(b/297542292): Remove this case once PSS profiling is replaced
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (mService.mAppProfiler.isProfilingPss()) {
pw.print(prefix);
pw.print("lastPssTime=");
TimeUtils.formatDuration(mLastPssTime, nowUptime, pw);
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 5ad921f..3391ec7 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -29,7 +29,6 @@
import android.annotation.ElapsedRealtimeLong;
import android.app.ActivityManager;
import android.content.ComponentName;
-import android.os.Flags;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Slog;
@@ -1351,7 +1350,7 @@
}
if (mNotCachedSinceIdle) {
pw.print(prefix); pw.print("notCachedSinceIdle="); pw.print(mNotCachedSinceIdle);
- if (!Flags.removeAppProfilerPssCollection()) {
+ if (mService.mAppProfiler.isProfilingPss()) {
pw.print(" initialIdlePss=");
} else {
pw.print(" initialIdleRss=");
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 3024dd2..8910b6e 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -82,6 +82,7 @@
public static final int AUTO_BRIGHTNESS_MODE_DEFAULT = 0;
public static final int AUTO_BRIGHTNESS_MODE_IDLE = 1;
public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
+ public static final int AUTO_BRIGHTNESS_MODE_MAX = AUTO_BRIGHTNESS_MODE_DOZE;
// How long the current sensor reading is assumed to be valid beyond the current time.
// This provides a bit of prediction, as well as ensures that the weight for the last sample is
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index e38d08f..bc3f9dd 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -4573,8 +4573,10 @@
if ((flags & DisplayDeviceInfo.FLAG_NEVER_BLANK) == 0) {
final DisplayPowerControllerInterface displayPowerController =
mDisplayPowerControllers.get(id);
- ready &= displayPowerController.requestPowerState(request,
- waitForNegativeProximity);
+ if (displayPowerController != null) {
+ ready &= displayPowerController.requestPowerState(request,
+ waitForNegativeProximity);
+ }
}
}
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 4e341a9..a43f93a 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -16,6 +16,8 @@
package com.android.server.display;
+import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_MAX;
+
import android.annotation.Nullable;
import android.hardware.display.DisplayManagerInternal;
import android.os.PowerManager;
@@ -65,6 +67,22 @@
return true;
}
+ @Override
+ public float[] getAutoBrightnessLevels(int mode) {
+ if (mode < 0 || mode > AUTO_BRIGHTNESS_MODE_MAX) {
+ throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
+ }
+ return mDisplayPowerController.getAutoBrightnessLevels(mode);
+ }
+
+ @Override
+ public float[] getAutoBrightnessLuxLevels(int mode) {
+ if (mode < 0 || mode > AUTO_BRIGHTNESS_MODE_MAX) {
+ throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
+ }
+ return mDisplayPowerController.getAutoBrightnessLuxLevels(mode);
+ }
+
/**
* Start the offload session. The method returns if the session is already active.
* @return Whether the session was started successfully
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 06e5f99..734381b 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2214,6 +2214,20 @@
}
@Override
+ public float[] getAutoBrightnessLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+ // The old DPC is no longer supported
+ return null;
+ }
+
+ @Override
+ public float[] getAutoBrightnessLuxLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+ // The old DPC is no longer supported
+ return null;
+ }
+
+ @Override
public BrightnessInfo getBrightnessInfo() {
synchronized (mCachedBrightnessInfo) {
return new BrightnessInfo(
diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java
index 519224a..7df6114 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController2.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController2.java
@@ -1886,6 +1886,24 @@
}
@Override
+ public float[] getAutoBrightnessLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+ int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+ Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+ return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels(mode, preset);
+ }
+
+ @Override
+ public float[] getAutoBrightnessLuxLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode) {
+ int preset = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
+ Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_NORMAL, UserHandle.USER_CURRENT);
+ return mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(mode, preset);
+ }
+
+ @Override
public BrightnessInfo getBrightnessInfo() {
synchronized (mCachedBrightnessInfo) {
return new BrightnessInfo(
diff --git a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
index c279184..13acb3f 100644
--- a/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
+++ b/services/core/java/com/android/server/display/DisplayPowerControllerInterface.java
@@ -237,4 +237,21 @@
* Indicate that boot has been completed and the screen is ready to update.
*/
void onBootCompleted();
+
+ /**
+ * Get the brightness levels used to determine automatic brightness based on lux levels.
+ * @param mode The auto-brightness mode
+ * @return The brightness levels for the specified mode. The values are between
+ * {@link PowerManager.BRIGHTNESS_MIN} and {@link PowerManager.BRIGHTNESS_MAX}.
+ */
+ float[] getAutoBrightnessLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode);
+
+ /**
+ * Get the lux levels used to determine automatic brightness.
+ * @param mode The auto-brightness mode
+ * @return The lux levels for the specified mode
+ */
+ float[] getAutoBrightnessLuxLevels(
+ @AutomaticBrightnessController.AutomaticBrightnessMode int mode);
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 24e23003..087c525 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2523,9 +2523,9 @@
// Native callback.
@SuppressWarnings("unused")
private int interceptMotionBeforeQueueingNonInteractive(int displayId,
- long whenNanos, int policyFlags) {
+ int source, int action, long whenNanos, int policyFlags) {
return mWindowManagerCallbacks.interceptMotionBeforeQueueingNonInteractive(
- displayId, whenNanos, policyFlags);
+ displayId, source, action, whenNanos, policyFlags);
}
// Native callback.
@@ -2901,8 +2901,8 @@
* processing when the device is in a non-interactive state since these events are normally
* dropped.
*/
- int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
- int policyFlags);
+ int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
+ long whenNanos, int policyFlags);
/**
* This callback is invoked just before the key is about to be sent to an application.
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 38f0df4..9088cb9 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -2700,11 +2700,8 @@
// session info from them.
sessionInfo = mSystemProvider.getDefaultSessionInfo();
}
- // TODO: b/279555229 - replace with matchingRequest.mRouterRecord.notifySessionCreated.
- notifySessionCreatedToRouter(
- matchingRequest.mRouterRecord,
- toOriginalRequestId(uniqueRequestId),
- sessionInfo);
+ matchingRequest.mRouterRecord.notifySessionCreated(
+ toOriginalRequestId(uniqueRequestId), sessionInfo);
}
private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider,
@@ -2812,11 +2809,6 @@
return true;
}
- private void notifySessionCreatedToRouter(@NonNull RouterRecord routerRecord,
- int requestId, @NonNull RoutingSessionInfo sessionInfo) {
- routerRecord.notifySessionCreated(requestId, sessionInfo);
- }
-
private void notifySessionCreationFailedToRouter(@NonNull RouterRecord routerRecord,
int requestId) {
try {
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index d4666ba..db39b5e 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -290,31 +290,12 @@
}
user.mPriorityStack.onSessionActiveStateChanged(record);
}
- notifyActivityManagerWithActiveStateChanges(record, record.isActive());
+ setForegroundServiceAllowance(
+ record, /* allowRunningInForeground= */ record.isActive());
mHandler.postSessionsChanged(record);
}
}
- private void notifyActivityManagerWithActiveStateChanges(
- MediaSessionRecordImpl record, boolean isActive) {
- if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
- return;
- }
- ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
- record.getForegroundServiceDelegationOptions();
- if (foregroundServiceDelegationOptions == null) {
- // This record doesn't support FGS delegation. In practice, this is MediaSession2.
- return;
- }
- if (isActive) {
- mActivityManagerInternal.startForegroundServiceDelegate(
- foregroundServiceDelegationOptions, /* connection= */ null);
- } else {
- mActivityManagerInternal.stopForegroundServiceDelegate(
- foregroundServiceDelegationOptions);
- }
- }
-
// Currently only media1 can become global priority session.
void setGlobalPrioritySession(MediaSessionRecord record) {
synchronized (mLock) {
@@ -407,27 +388,10 @@
return;
}
user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
- notifyActivityManagerWithPlaybackStateChanges(record, playbackState);
- }
- }
-
- private void notifyActivityManagerWithPlaybackStateChanges(
- MediaSessionRecordImpl record, PlaybackState playbackState) {
- if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
- return;
- }
- ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
- record.getForegroundServiceDelegationOptions();
- if (foregroundServiceDelegationOptions == null || playbackState == null) {
- // This record doesn't support FGS delegation. In practice, this is MediaSession2.
- return;
- }
- if (playbackState.shouldAllowServiceToRunInForeground()) {
- mActivityManagerInternal.startForegroundServiceDelegate(
- foregroundServiceDelegationOptions, /* connection= */ null);
- } else {
- mActivityManagerInternal.stopForegroundServiceDelegate(
- foregroundServiceDelegationOptions);
+ if (playbackState != null) {
+ setForegroundServiceAllowance(
+ record, playbackState.shouldAllowServiceToRunInForeground());
+ }
}
}
@@ -591,11 +555,12 @@
}
session.close();
- notifyActivityManagerWithSessionDestroyed(session);
+ setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false);
mHandler.postSessionsChanged(session);
}
- private void notifyActivityManagerWithSessionDestroyed(MediaSessionRecordImpl record) {
+ private void setForegroundServiceAllowance(
+ MediaSessionRecordImpl record, boolean allowRunningInForeground) {
if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
return;
}
@@ -605,7 +570,13 @@
// This record doesn't support FGS delegation. In practice, this is MediaSession2.
return;
}
- mActivityManagerInternal.stopForegroundServiceDelegate(foregroundServiceDelegationOptions);
+ if (allowRunningInForeground) {
+ mActivityManagerInternal.startForegroundServiceDelegate(
+ foregroundServiceDelegationOptions, /* connection= */ null);
+ } else {
+ mActivityManagerInternal.stopForegroundServiceDelegate(
+ foregroundServiceDelegationOptions);
+ }
}
void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage,
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 1660c3e..e546f42 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -21,13 +21,11 @@
import android.Manifest;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
-import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.admin.DevicePolicyManager;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.os.Binder;
import android.os.BugreportManager.BugreportCallback;
import android.os.BugreportParams;
@@ -39,6 +37,7 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -96,6 +95,7 @@
private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000;
private final Object mLock = new Object();
+ private final Injector mInjector;
private final Context mContext;
private final AppOpsManager mAppOps;
private final TelephonyManager mTelephonyManager;
@@ -346,6 +346,14 @@
AtomicFile getMappingFile() {
return mMappingFile;
}
+
+ UserManager getUserManager() {
+ return mContext.getSystemService(UserManager.class);
+ }
+
+ DevicePolicyManager getDevicePolicyManager() {
+ return mContext.getSystemService(DevicePolicyManager.class);
+ }
}
BugreportManagerServiceImpl(Context context) {
@@ -357,6 +365,7 @@
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
BugreportManagerServiceImpl(Injector injector) {
+ mInjector = injector;
mContext = injector.getContext();
mAppOps = mContext.getSystemService(AppOpsManager.class);
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
@@ -389,12 +398,7 @@
int callingUid = Binder.getCallingUid();
enforcePermission(callingPackage, callingUid, bugreportMode
== BugreportParams.BUGREPORT_MODE_TELEPHONY /* checkCarrierPrivileges */);
- final long identity = Binder.clearCallingIdentity();
- try {
- ensureUserCanTakeBugReport(bugreportMode);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
+ ensureUserCanTakeBugReport(bugreportMode);
Slogf.i(TAG, "Starting bugreport for %s / %d", callingPackage, callingUid);
synchronized (mLock) {
@@ -433,7 +437,6 @@
@RequiresPermission(value = Manifest.permission.DUMP, conditional = true)
public void retrieveBugreport(int callingUidUnused, String callingPackage, int userId,
FileDescriptor bugreportFd, String bugreportFile,
-
boolean keepBugreportOnRetrievalUnused, IDumpstateListener listener) {
int callingUid = Binder.getCallingUid();
enforcePermission(callingPackage, callingUid, false);
@@ -565,54 +568,48 @@
}
/**
- * Validates that the current user is an admin user or, when bugreport is requested remotely
- * that the current user is an affiliated user.
+ * Validates that the calling user is an admin user or, when bugreport is requested remotely
+ * that the user is an affiliated user.
*
- * @throws IllegalArgumentException if the current user is not an admin user
+ * @throws IllegalArgumentException if the calling user is not an admin user
*/
private void ensureUserCanTakeBugReport(int bugreportMode) {
- UserInfo currentUser = null;
+ // Get the calling userId before clearing the caller identity.
+ int callingUserId = UserHandle.getUserId(Binder.getCallingUid());
+ boolean isAdminUser = false;
+ final long identity = Binder.clearCallingIdentity();
try {
- currentUser = ActivityManager.getService().getCurrentUser();
- } catch (RemoteException e) {
- // Impossible to get RemoteException for an in-process call.
+ isAdminUser = mInjector.getUserManager().isUserAdmin(callingUserId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
-
- if (currentUser == null) {
- logAndThrow("There is no current user, so no bugreport can be requested.");
- }
-
- if (!currentUser.isAdmin()) {
+ if (!isAdminUser) {
if (bugreportMode == BugreportParams.BUGREPORT_MODE_REMOTE
- && isCurrentUserAffiliated(currentUser.id)) {
+ && isUserAffiliated(callingUserId)) {
return;
}
- logAndThrow(TextUtils.formatSimple("Current user %s is not an admin user."
- + " Only admin users are allowed to take bugreport.", currentUser.id));
+ logAndThrow(TextUtils.formatSimple("Calling user %s is not an admin user."
+ + " Only admin users are allowed to take bugreport.", callingUserId));
}
}
/**
- * Returns {@code true} if the device has device owner and the current user is affiliated
+ * Returns {@code true} if the device has device owner and the specified user is affiliated
* with the device owner.
*/
- private boolean isCurrentUserAffiliated(int currentUserId) {
- DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+ private boolean isUserAffiliated(int userId) {
+ DevicePolicyManager dpm = mInjector.getDevicePolicyManager();
int deviceOwnerUid = dpm.getDeviceOwnerUserId();
if (deviceOwnerUid == UserHandle.USER_NULL) {
return false;
}
- int callingUserId = UserHandle.getUserId(Binder.getCallingUid());
-
- Slog.i(TAG, "callingUid: " + callingUserId + " deviceOwnerUid: " + deviceOwnerUid
- + " currentUserId: " + currentUserId);
-
- if (callingUserId != deviceOwnerUid) {
- logAndThrow("Caller is not device owner on provisioned device.");
+ if (DEBUG) {
+ Slog.d(TAG, "callingUid: " + userId + " deviceOwnerUid: " + deviceOwnerUid);
}
- if (!dpm.isAffiliatedUser(currentUserId)) {
- logAndThrow("Current user is not affiliated to the device owner.");
+
+ if (userId != deviceOwnerUid && !dpm.isAffiliatedUser(userId)) {
+ logAndThrow("User " + userId + " is not affiliated to the device owner.");
}
return true;
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index edae273..b286b12 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -64,6 +64,7 @@
import android.os.Message;
import android.os.PatternMatcher;
import android.os.PersistableBundle;
+import android.os.Process;
import android.os.SELinux;
import android.os.SystemClock;
import android.os.Trace;
@@ -3189,6 +3190,9 @@
pkg.isScannedAsStoppedSystemApp());
if (!pkg.hasSharedUser()) {
serializer.attributeInt(null, "userId", pkg.getAppId());
+
+ serializer.attributeBoolean(null, "isSdkLibrary",
+ pkg.getAndroidPackage() != null && pkg.getAndroidPackage().isSdkLibrary());
} else {
serializer.attributeInt(null, "sharedUserId", pkg.getAppId());
}
@@ -4039,10 +4043,12 @@
int targetSdkVersion = 0;
byte[] restrictUpdateHash = null;
boolean isScannedAsStoppedSystemApp = false;
+ boolean isSdkLibrary = false;
try {
name = parser.getAttributeValue(null, ATTR_NAME);
realName = parser.getAttributeValue(null, "realName");
appId = parseAppId(parser);
+ isSdkLibrary = parser.getAttributeBoolean(null, "isSdkLibrary", false);
sharedUserAppId = parseSharedUserAppId(parser);
codePathStr = parser.getAttributeValue(null, "codePath");
@@ -4157,7 +4163,8 @@
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <package> has no codePath at "
+ parser.getPositionDescription());
- } else if (appId > 0) {
+ } else if (appId > 0 || (appId == Process.INVALID_UID && isSdkLibrary
+ && Flags.disallowSdkLibsToBeApps())) {
packageSetting = addPackageLPw(name.intern(), realName, new File(codePathStr),
appId, pkgFlags, pkgPrivateFlags, domainSetId);
if (PackageManagerService.DEBUG_SETTINGS)
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index d3931a3..10e6edc 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -26,6 +26,7 @@
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_ERRORED;
import static android.app.AppOpsManager.MODE_IGNORED;
+import static android.app.AppOpsManager.OP_BLUETOOTH_CONNECT;
import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISALLOWED;
import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISCOURAGED;
@@ -1228,6 +1229,11 @@
sPlatformPermissions.put(permission, permissionInfo);
}
} catch (PackageManager.NameNotFoundException ignored) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as package"
+ + " not found when retrieving permission info");
+ }
return PermissionChecker.PERMISSION_HARD_DENIED;
}
}
@@ -1347,17 +1353,34 @@
// way we can avoid the datasource creating an attribution context for every call.
if (!(fromDatasource && current.equals(attributionSource))
&& next != null && !current.isTrusted(context)) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as "
+ + current + " attribution source isn't a data source and "
+ + current + " isn't trusted");
+ }
return PermissionChecker.PERMISSION_HARD_DENIED;
}
// If we already checked the permission for this one, skip the work
if (!skipCurrentChecks && !checkPermission(context, permissionManagerServiceInt,
permission, current)) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as we"
+ + " aren't skipping permission checks and permission check returns"
+ + " false for " + current);
+ }
return PermissionChecker.PERMISSION_HARD_DENIED;
}
if (next != null && !checkPermission(context, permissionManagerServiceInt,
permission, next)) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as"
+ + " permission check returns false for next source " + next);
+ }
return PermissionChecker.PERMISSION_HARD_DENIED;
}
@@ -1402,6 +1425,10 @@
switch (opMode) {
case AppOpsManager.MODE_ERRORED: {
+ if (permission.equals(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as op"
+ + " mode is MODE_ERRORED for " + attributionSource);
+ }
return PermissionChecker.PERMISSION_HARD_DENIED;
}
case AppOpsManager.MODE_IGNORED: {
@@ -1670,6 +1697,12 @@
final AttributionSource resolvedAttributionSource = resolveAttributionSource(
context, accessorSource);
if (resolvedAttributionSource.getPackageName() == null) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (op == OP_BLUETOOTH_CONNECT) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as resolved"
+ + "package name for " + resolvedAttributionSource + " returned"
+ + " null");
+ }
return AppOpsManager.MODE_ERRORED;
}
int notedOp = op;
@@ -1683,6 +1716,13 @@
if (attributedOp != AppOpsManager.OP_NONE && attributedOp != op) {
checkedOpResult = appOpsManager.checkOpNoThrow(op, resolvedAttributionSource);
if (checkedOpResult == MODE_ERRORED) {
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (op == OP_BLUETOOTH_CONNECT) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as"
+ + " checkOp for resolvedAttributionSource "
+ + resolvedAttributionSource + " and op " + op
+ + " returned MODE_ERRORED");
+ }
return checkedOpResult;
}
notedOp = attributedOp;
@@ -1722,7 +1762,22 @@
throw new SecurityException(msg + ":" + e.getMessage());
}
}
- return Math.max(checkedOpResult, notedOpResult);
+ int result = Math.max(checkedOpResult, notedOpResult);
+ // TODO(b/302609140): Remove extra logging after this issue is diagnosed.
+ if (op == OP_BLUETOOTH_CONNECT && result == MODE_ERRORED) {
+ if (result == checkedOpResult) {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as"
+ + " checkOp for resolvedAttributionSource "
+ + resolvedAttributionSource + " and op " + op
+ + " returned MODE_ERRORED");
+ } else {
+ Slog.e(LOG_TAG, "BLUETOOTH_CONNECT permission hard denied as"
+ + " noteOp for resolvedAttributionSource "
+ + resolvedAttributionSource + " and op " + notedOp
+ + " returned MODE_ERRORED");
+ }
+ }
+ return result;
}
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 3000a1c..5b13d3fe 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -5201,8 +5201,8 @@
// TODO(b/117479243): handle it in InputPolicy
/** {@inheritDoc} */
@Override
- public int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
- int policyFlags) {
+ public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
+ long whenNanos, int policyFlags) {
if ((policyFlags & FLAG_WAKE) != 0) {
if (wakeUp(whenNanos / 1000000, mAllowTheaterModeWakeFromMotion,
PowerManager.WAKE_REASON_WAKE_MOTION, "android.policy:MOTION")) {
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 03a7bd3..3016b39 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -706,12 +706,14 @@
* Generally, it's best to keep as little as possible in the queue thread
* because it's the most fragile.
* @param displayId The display ID of the motion event.
+ * @param source the {@link InputDevice} source that caused the motion.
+ * @param action the {@link MotionEvent} action for the motion.
* @param policyFlags The policy flags associated with the motion.
*
* @return Actions flags: may be {@link #ACTION_PASS_TO_USER}.
*/
- int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
- int policyFlags);
+ int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
+ long whenNanos, int policyFlags);
/**
* Called from the input dispatcher thread before a key is dispatched to a window.
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 8cf4713..a84ebd9 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -167,10 +167,10 @@
/** {@inheritDoc} */
@Override
- public int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
- int policyFlags) {
+ public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
+ long whenNanos, int policyFlags) {
return mService.mPolicy.interceptMotionBeforeQueueingNonInteractive(
- displayId, whenNanos, policyFlags);
+ displayId, source, action, whenNanos, policyFlags);
}
/**
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 9305396..97cc982 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -46,6 +46,7 @@
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
@@ -1185,6 +1186,7 @@
mUserAspectRatio = getUserMinAspectRatioOverrideCode();
return mUserAspectRatio != USER_MIN_ASPECT_RATIO_UNSET
+ && mUserAspectRatio != USER_MIN_ASPECT_RATIO_APP_DEFAULT
&& mUserAspectRatio != USER_MIN_ASPECT_RATIO_FULLSCREEN;
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 0dd0564..9ba0a2a 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -358,8 +358,8 @@
void notifyVibratorState(int32_t deviceId, bool isOn) override;
bool filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) override;
void interceptKeyBeforeQueueing(const KeyEvent& keyEvent, uint32_t& policyFlags) override;
- void interceptMotionBeforeQueueing(int32_t displayId, nsecs_t when,
- uint32_t& policyFlags) override;
+ void interceptMotionBeforeQueueing(int32_t displayId, uint32_t source, int32_t action,
+ nsecs_t when, uint32_t& policyFlags) override;
nsecs_t interceptKeyBeforeDispatching(const sp<IBinder>& token, const KeyEvent& keyEvent,
uint32_t policyFlags) override;
std::optional<KeyEvent> dispatchUnhandledKey(const sp<IBinder>& token, const KeyEvent& keyEvent,
@@ -1496,7 +1496,8 @@
handleInterceptActions(wmActions, when, /*byref*/ policyFlags);
}
-void NativeInputManager::interceptMotionBeforeQueueing(int32_t displayId, nsecs_t when,
+void NativeInputManager::interceptMotionBeforeQueueing(int32_t displayId, uint32_t source,
+ int32_t action, nsecs_t when,
uint32_t& policyFlags) {
ATRACE_CALL();
// Policy:
@@ -1525,7 +1526,7 @@
const jint wmActions =
env->CallIntMethod(mServiceObj,
gServiceClassInfo.interceptMotionBeforeQueueingNonInteractive,
- displayId, when, policyFlags);
+ displayId, source, action, when, policyFlags);
if (checkAndClearExceptionFromCallback(env, "interceptMotionBeforeQueueingNonInteractive")) {
return;
}
@@ -2943,7 +2944,7 @@
"interceptKeyBeforeQueueing", "(Landroid/view/KeyEvent;I)I");
GET_METHOD_ID(gServiceClassInfo.interceptMotionBeforeQueueingNonInteractive, clazz,
- "interceptMotionBeforeQueueingNonInteractive", "(IJI)I");
+ "interceptMotionBeforeQueueingNonInteractive", "(IIIJI)I");
GET_METHOD_ID(gServiceClassInfo.interceptKeyBeforeDispatching, clazz,
"interceptKeyBeforeDispatching",
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index dc1d2c5..21b8a94 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -16,23 +16,26 @@
package com.android.server.os;
-import android.app.admin.flags.Flags;
-import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
-
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
import android.app.role.RoleManager;
import android.content.Context;
import android.os.Binder;
import android.os.BugreportManager.BugreportCallback;
+import android.os.BugreportParams;
import android.os.IBinder;
import android.os.IDumpstateListener;
import android.os.Process;
import android.os.RemoteException;
+import android.os.UserManager;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -48,6 +51,8 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.util.concurrent.CompletableFuture;
@@ -66,6 +71,11 @@
private BugreportManagerServiceImpl mService;
private BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager;
+ @Mock
+ private UserManager mMockUserManager;
+ @Mock
+ private DevicePolicyManager mMockDevicePolicyManager;
+
private int mCallingUid = 1234;
private String mCallingPackage = "test.package";
private AtomicFile mMappingFile;
@@ -75,14 +85,17 @@
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml");
ArraySet<String> mAllowlistedPackages = new ArraySet<>();
mAllowlistedPackages.add(mContext.getPackageName());
mService = new BugreportManagerServiceImpl(
- new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages,
- mMappingFile));
+ new TestInjector(mContext, mAllowlistedPackages, mMappingFile,
+ mMockUserManager, mMockDevicePolicyManager));
mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile);
+ // The calling user is an admin user by default.
+ when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(true);
}
@After
@@ -165,6 +178,36 @@
}
@Test
+ public void testStartBugreport_throwsForNonAdminUser() throws Exception {
+ when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
+
+ Exception thrown = assertThrows(Exception.class,
+ () -> mService.startBugreport(mCallingUid, mContext.getPackageName(),
+ new FileDescriptor(), /* screenshotFd= */ null,
+ BugreportParams.BUGREPORT_MODE_FULL,
+ /* flags= */ 0, new Listener(new CountDownLatch(1)),
+ /* isScreenshotRequested= */ false));
+
+ assertThat(thrown.getMessage()).contains("not an admin user");
+ }
+
+ @Test
+ public void testStartBugreport_throwsForNotAffiliatedUser() throws Exception {
+ when(mMockUserManager.isUserAdmin(anyInt())).thenReturn(false);
+ when(mMockDevicePolicyManager.getDeviceOwnerUserId()).thenReturn(-1);
+ when(mMockDevicePolicyManager.isAffiliatedUser(anyInt())).thenReturn(false);
+
+ Exception thrown = assertThrows(Exception.class,
+ () -> mService.startBugreport(mCallingUid, mContext.getPackageName(),
+ new FileDescriptor(), /* screenshotFd= */ null,
+ BugreportParams.BUGREPORT_MODE_REMOTE,
+ /* flags= */ 0, new Listener(new CountDownLatch(1)),
+ /* isScreenshotRequested= */ false));
+
+ assertThat(thrown.getMessage()).contains("not affiliated to the device owner");
+ }
+
+ @Test
public void testRetrieveBugreportWithoutFilesForCaller() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
Listener listener = new Listener(latch);
@@ -207,7 +250,8 @@
private void clearAllowlist() {
mService = new BugreportManagerServiceImpl(
- new BugreportManagerServiceImpl.Injector(mContext, new ArraySet<>(), mMappingFile));
+ new TestInjector(mContext, new ArraySet<>(), mMappingFile,
+ mMockUserManager, mMockDevicePolicyManager));
}
private static class Listener implements IDumpstateListener {
@@ -258,4 +302,27 @@
complete(successful);
}
}
+
+ private static class TestInjector extends BugreportManagerServiceImpl.Injector {
+
+ private final UserManager mUserManager;
+ private final DevicePolicyManager mDevicePolicyManager;
+
+ TestInjector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile,
+ UserManager um, DevicePolicyManager dpm) {
+ super(context, allowlistedPackages, mappingFile);
+ mUserManager = um;
+ mDevicePolicyManager = dpm;
+ }
+
+ @Override
+ public UserManager getUserManager() {
+ return mUserManager;
+ }
+
+ @Override
+ public DevicePolicyManager getDevicePolicyManager() {
+ return mDevicePolicyManager;
+ }
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index bd111ad..52e2d8a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -89,8 +89,8 @@
}
@Override
- public int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
- int policyFlags) {
+ public int interceptMotionBeforeQueueingNonInteractive(int displayId, int source, int action,
+ long whenNanos, int policyFlags) {
return 0;
}
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
index 25d208d..5cbb1aa 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt
@@ -77,7 +77,9 @@
.autoFix()
.build()
- return LintFix.create().composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
+ return LintFix.create()
+ .name(annotateFix.getDisplayName())
+ .composite(annotateFix, *replaceOrRemoveFixes.toTypedArray())
}
private val annotation: String