Implement uninstall behaviour for archiving

Test: ArchiveManagerTest
Bug: 282952870

Change-Id: I8d9d432afa313866e9810a52b48b9c6277e80ff2
This commit is contained in:
Jakob Schneider
2023-07-03 18:06:57 +01:00
parent 272f482d1f
commit 63c0269680
3 changed files with 340 additions and 154 deletions

View File

@@ -16,14 +16,28 @@
package com.android.server.pm;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.IntentSender;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.os.Binder;
import android.os.UserHandle;
import android.text.TextUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.server.pm.pkg.ArchiveState;
import com.android.server.pm.pkg.ArchiveState.ArchiveActivityInfo;
import com.android.server.pm.pkg.PackageStateInternal;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
@@ -35,37 +49,103 @@ import java.util.Objects;
*/
final class ArchiveManager {
private final Context mContext;
private final PackageManagerService mPm;
ArchiveManager(PackageManagerService mPm) {
@Nullable
private LauncherApps mLauncherApps;
ArchiveManager(Context context, PackageManagerService mPm) {
this.mContext = context;
this.mPm = mPm;
}
void archiveApp(
@NonNull String packageName,
@NonNull String callerPackageName,
int userId,
@NonNull UserHandle user,
@NonNull IntentSender intentSender) throws PackageManager.NameNotFoundException {
Objects.requireNonNull(packageName);
Objects.requireNonNull(callerPackageName);
Objects.requireNonNull(user);
Objects.requireNonNull(intentSender);
Computer snapshot = mPm.snapshotComputer();
int callingUid = Binder.getCallingUid();
int userId = user.getIdentifier();
String callingPackageName = snapshot.getNameForUid(callingUid);
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "archiveApp");
snapshot.enforceCrossUserPermission(callingUid, userId, true, true,
"archiveApp");
verifyCaller(callerPackageName, callingPackageName);
PackageStateInternal ps = snapshot.getPackageStateInternal(packageName);
PackageStateInternal ps = getPackageState(packageName, snapshot, callingUid, user);
verifyInstallOwnership(packageName, callingPackageName, ps.getInstallSource());
List<LauncherActivityInfo> mainActivities = getLauncherApps().getActivityList(
ps.getPackageName(),
new UserHandle(userId));
// TODO(b/291569242) Verify that this list is not empty and return failure with intentsender
storeArchiveState(ps, mainActivities, userId);
// TODO(b/278553670) Add special strings for the delete dialog
mPm.mInstallerService.uninstall(
new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
callerPackageName, DELETE_KEEP_DATA, intentSender, userId);
}
@NonNull
private static PackageStateInternal getPackageState(String packageName,
Computer snapshot, int callingUid, UserHandle user)
throws PackageManager.NameNotFoundException {
PackageStateInternal ps = snapshot.getPackageStateFiltered(packageName, callingUid,
user.getIdentifier());
if (ps == null) {
throw new PackageManager.NameNotFoundException(
TextUtils.formatSimple("Package %s not found.", packageName));
}
return ps;
}
verifyInstallOwnership(packageName, callingPackageName, ps);
private LauncherApps getLauncherApps() {
if (mLauncherApps == null) {
mLauncherApps = mContext.getSystemService(LauncherApps.class);
}
return mLauncherApps;
}
// TODO(b/278553670) Complete implementations
throw new UnsupportedOperationException("Method not implemented.");
private void storeArchiveState(PackageStateInternal ps,
List<LauncherActivityInfo> mainActivities, int userId)
throws PackageManager.NameNotFoundException {
List<ArchiveActivityInfo> activityInfos = new ArrayList<>();
for (int i = 0; i < mainActivities.size(); i++) {
// TODO(b/278553670) Extract and store launcher icons
ArchiveActivityInfo activityInfo = new ArchiveActivityInfo(
mainActivities.get(i).getLabel().toString(),
Path.of("/TODO"), null);
activityInfos.add(activityInfo);
}
// TODO(b/278553670) Adapt installer check verifyInstallOwnership and check for null there
InstallSource installSource = ps.getInstallSource();
String installerPackageName = installSource.mUpdateOwnerPackageName != null
? installSource.mUpdateOwnerPackageName : installSource.mInstallerPackageName;
synchronized (mPm.mLock) {
getPackageSetting(ps.getPackageName(), userId).modifyUserState(userId).setArchiveState(
new ArchiveState(activityInfos, installerPackageName));
}
}
@NonNull
@GuardedBy("mPm.mLock")
private PackageSetting getPackageSetting(String packageName, int userId)
throws PackageManager.NameNotFoundException {
PackageSetting ps = mPm.mSettings.getPackageLPr(packageName);
if (ps == null || !ps.getUserStateOrDefault(userId).isInstalled()) {
throw new PackageManager.NameNotFoundException(
TextUtils.formatSimple("Package %s not found.", packageName));
}
return ps;
}
private static void verifyCaller(String callerPackageName, String callingPackageName) {
@@ -80,14 +160,14 @@ final class ArchiveManager {
}
private static void verifyInstallOwnership(String packageName, String callingPackageName,
PackageStateInternal ps) {
if (!TextUtils.equals(ps.getInstallSource().mInstallerPackageName,
InstallSource installSource) {
if (!TextUtils.equals(installSource.mInstallerPackageName,
callingPackageName)) {
throw new SecurityException(
TextUtils.formatSimple("Caller is not the installer of record for %s.",
packageName));
}
String updateOwnerPackageName = ps.getInstallSource().mUpdateOwnerPackageName;
String updateOwnerPackageName = installSource.mUpdateOwnerPackageName;
if (updateOwnerPackageName != null
&& !TextUtils.equals(updateOwnerPackageName, callingPackageName)) {
throw new SecurityException(

View File

@@ -0,0 +1,250 @@
/*
* 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.server.pm;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.IntentSender;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.os.Binder;
import android.os.Build;
import android.os.Process;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.pm.pkg.ArchiveState;
import com.android.server.pm.pkg.PackageStateInternal;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class ArchiveManagerTest {
private static final String PACKAGE = "com.example";
private static final String CALLER_PACKAGE = "com.vending";
@Rule
public final MockSystemRule mMockSystem = new MockSystemRule();
@Mock
private IntentSender mIntentSender;
@Mock
private Computer mComputer;
@Mock
private Context mContext;
@Mock
private LauncherApps mLauncherApps;
@Mock
private PackageInstallerService mInstallerService;
@Mock
private PackageStateInternal mPackageState;
private final InstallSource mInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
CALLER_PACKAGE,
Binder.getCallingUid(),
CALLER_PACKAGE,
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
private final List<LauncherActivityInfo> mLauncherActivityInfos = createLauncherActivities();
private PackageSetting mPackageSetting;
private PackageManagerService mPm;
private ArchiveManager mArchiveManager;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mMockSystem.system().stageNominalSystemState();
when(mMockSystem.mocks().getInjector().getPackageInstallerService()).thenReturn(
mInstallerService);
mPm = spy(new PackageManagerService(mMockSystem.mocks().getInjector(),
/* factoryTest= */false,
MockSystem.Companion.getDEFAULT_VERSION_INFO().fingerprint,
/* isEngBuild= */ false,
/* isUserDebugBuild= */false,
Build.VERSION_CODES.CUR_DEVELOPMENT,
Build.VERSION.INCREMENTAL));
when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
mPackageState);
when(mPackageState.getPackageName()).thenReturn(PACKAGE);
when(mPackageState.getInstallSource()).thenReturn(mInstallSource);
mPackageSetting = createBasicPackageSetting();
when(mMockSystem.mocks().getSettings().getPackageLPr(eq(PACKAGE))).thenReturn(
mPackageSetting);
when(mContext.getSystemService(LauncherApps.class)).thenReturn(mLauncherApps);
when(mLauncherApps.getActivityList(eq(PACKAGE), eq(UserHandle.CURRENT))).thenReturn(
mLauncherActivityInfos);
doReturn(mComputer).when(mPm).snapshotComputer();
when(mComputer.getNameForUid(eq(Binder.getCallingUid()))).thenReturn(CALLER_PACKAGE);
mArchiveManager = new ArchiveManager(mContext, mPm);
}
@Test
public void archiveApp_callerPackageNameIncorrect() {
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, "different", UserHandle.CURRENT,
mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format(
"The callerPackageName %s set by the caller doesn't match the "
+ "caller's own package name %s.",
"different",
CALLER_PACKAGE));
}
@Test
public void archiveApp_packageNotInstalled() {
when(mMockSystem.mocks().getSettings().getPackageLPr(eq(PACKAGE))).thenReturn(
null);
Exception e = assertThrows(
PackageManager.NameNotFoundException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT,
mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(String.format("Package %s not found.", PACKAGE));
}
@Test
public void archiveApp_packageNotInstalledForUser() {
mPackageSetting.modifyUserState(UserHandle.CURRENT.getIdentifier()).setInstalled(false);
Exception e = assertThrows(
PackageManager.NameNotFoundException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT,
mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(String.format("Package %s not found.", PACKAGE));
}
@Test
public void archiveApp_callerNotInstallerOfRecord() {
InstallSource otherInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
/* installerPackageName= */ "different",
Binder.getCallingUid(),
CALLER_PACKAGE,
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, Process.myUserHandle(),
mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format("Caller is not the installer of record for %s.", PACKAGE));
}
@Test
public void archiveApp_callerNotUpdateOwner() {
InstallSource otherInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
CALLER_PACKAGE,
Binder.getCallingUid(),
/* updateOwnerPackageName= */ "different",
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT,
mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format("Caller is not the update owner for %s.", PACKAGE));
}
@Test
public void archiveApp_success() throws PackageManager.NameNotFoundException {
List<ArchiveState.ArchiveActivityInfo> activityInfos = new ArrayList<>();
for (LauncherActivityInfo mainActivity : createLauncherActivities()) {
// TODO(b/278553670) Extract and store launcher icons
ArchiveState.ArchiveActivityInfo activityInfo = new ArchiveState.ArchiveActivityInfo(
mainActivity.getLabel().toString(),
Path.of("/TODO"), null);
activityInfos.add(activityInfo);
}
mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, UserHandle.CURRENT, mIntentSender);
verify(mInstallerService).uninstall(
eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)),
eq(CALLER_PACKAGE), eq(DELETE_KEEP_DATA), eq(mIntentSender),
eq(UserHandle.CURRENT.getIdentifier()));
assertThat(mPackageSetting.readUserState(
UserHandle.CURRENT.getIdentifier()).getArchiveState()).isEqualTo(
new ArchiveState(activityInfos, CALLER_PACKAGE));
}
private static List<LauncherActivityInfo> createLauncherActivities() {
LauncherActivityInfo activity1 = mock(LauncherActivityInfo.class);
when(activity1.getLabel()).thenReturn("activity1");
LauncherActivityInfo activity2 = mock(LauncherActivityInfo.class);
when(activity2.getLabel()).thenReturn("activity2");
return List.of(activity1, activity2);
}
private PackageSetting createBasicPackageSetting() {
return new PackageSettingBuilder()
.setName(PACKAGE).setCodePath("/data/app/" + PACKAGE + "-randompath")
.setInstallState(UserHandle.CURRENT.getIdentifier(), /* installed= */ true)
.build();
}
}

View File

@@ -1,144 +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.server.pm;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.pm.pkg.PackageStateInternal;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class ArchiveManagerTest {
private static final String PACKAGE = "com.example";
private static final String CALLER_PACKAGE = "com.vending";
private static final int USER_ID = 1;
@Mock private IntentSender mIntentSender;
@Mock private PackageManagerService mPm;
@Mock private Computer mComputer;
@Mock private PackageStateInternal mPackageState;
private InstallSource mInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
CALLER_PACKAGE,
Binder.getCallingUid(),
CALLER_PACKAGE,
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
private ArchiveManager mArchiveManager;
private int mCallingUid;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mArchiveManager = new ArchiveManager(mPm);
mCallingUid = Binder.getCallingUid();
when(mPm.snapshotComputer()).thenReturn(mComputer);
when(mComputer.getNameForUid(eq(mCallingUid))).thenReturn(CALLER_PACKAGE);
when(mComputer.getPackageStateInternal(eq(PACKAGE))).thenReturn(mPackageState);
when(mPackageState.getInstallSource()).thenReturn(mInstallSource);
}
@Test
public void archiveApp_callerPackageNameIncorrect() {
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, "different", USER_ID, mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format(
"The callerPackageName %s set by the caller doesn't match the "
+ "caller's own package name %s.",
"different",
CALLER_PACKAGE));
}
@Test
public void archiveApp_packageNotInstalled() {
when(mComputer.getPackageStateInternal(eq(PACKAGE))).thenReturn(null);
Exception e = assertThrows(
PackageManager.NameNotFoundException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(String.format("Package %s not found.", PACKAGE));
}
@Test
public void archiveApp_callerNotInstallerOfRecord() {
InstallSource otherInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
/* installerPackageName= */ "different",
Binder.getCallingUid(),
CALLER_PACKAGE,
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format("Caller is not the installer of record for %s.", PACKAGE));
}
@Test
public void archiveApp_callerNotUpdateOwner() {
InstallSource otherInstallSource =
InstallSource.create(
CALLER_PACKAGE,
CALLER_PACKAGE,
CALLER_PACKAGE,
Binder.getCallingUid(),
/* updateOwnerPackageName= */ "different",
/* installerAttributionTag= */ null,
/* packageSource= */ 0);
when(mPackageState.getInstallSource()).thenReturn(otherInstallSource);
Exception e = assertThrows(
SecurityException.class,
() -> mArchiveManager.archiveApp(PACKAGE, CALLER_PACKAGE, USER_ID, mIntentSender)
);
assertThat(e).hasMessageThat().isEqualTo(
String.format("Caller is not the update owner for %s.", PACKAGE));
}
}