pkgStates = new ArrayList<>();
skipToTag(parser, TAG_PERMISSION_BACKUP);
int backupPlatformVersion;
try {
backupPlatformVersion = Integer.parseInt(
parser.getAttributeValue(null, ATTR_PLATFORM_VERSION));
} catch (NumberFormatException ignored) {
// Platforms P and before did not store the platform version
backupPlatformVersion = Build.VERSION_CODES.P;
}
skipToTag(parser, TAG_ALL_GRANTS);
if (parser.getEventType() != START_TAG && !parser.getName().equals(TAG_ALL_GRANTS)) {
throw new XmlPullParserException("Could not find " + TAG_PERMISSION_BACKUP + " > "
+ TAG_ALL_GRANTS);
}
// Read packages to restore from xml
int type;
do {
type = parser.next();
switch (type) {
case START_TAG:
switch (parser.getName()) {
case TAG_GRANT:
try {
pkgStates.add(BackupPackageState.parseFromXml(parser, mContext,
backupPlatformVersion));
} catch (XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse permissions ", e);
skipToEndOfTag(parser);
}
break;
default:
// ignore tag
Log.w(LOG_TAG, "Found unexpected tag " + parser.getName()
+ " during restore");
skipToEndOfTag(parser);
}
}
} while (type != END_DOCUMENT);
return pkgStates;
}
/**
* Try to restore the permission state from XML.
*
* If some apps could not be restored, the leftover apps are written to
* {@link Constants#DELAYED_RESTORE_PERMISSIONS_FILE}.
*
* @param parser The xml to read
*/
void restoreState(@NonNull XmlPullParser parser) throws IOException, XmlPullParserException {
ArrayList pkgStates = parseFromXml(parser);
ArrayList packagesToRestoreLater = new ArrayList<>();
int numPkgStates = pkgStates.size();
if (numPkgStates > 0) {
// Try to restore packages
for (int i = 0; i < numPkgStates; i++) {
BackupPackageState pkgState = pkgStates.get(i);
PackageInfo pkgInfo;
try {
pkgInfo = mContext.getPackageManager().getPackageInfo(pkgState.mPackageName,
GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException ignored) {
packagesToRestoreLater.add(pkgState);
continue;
}
pkgState.restore(mContext, pkgInfo);
}
}
synchronized (sLock) {
writeDelayedStorePkgsLocked(packagesToRestoreLater);
}
}
/**
* Write a xml file for the given packages.
*
* @param serializer The file to write to
* @param pkgs The packages to write
*/
private static void writePkgsAsXml(@NonNull XmlSerializer serializer,
@NonNull ArrayList pkgs) throws IOException {
serializer.startDocument(null, true);
serializer.startTag(null, TAG_PERMISSION_BACKUP);
if (BuildCompat.isAtLeastQ()) {
// STOPSHIP: Remove compatibility code once Q SDK level is declared
serializer.attribute(null, ATTR_PLATFORM_VERSION,
Integer.valueOf(Build.VERSION_CODES.Q).toString());
} else {
serializer.attribute(null, ATTR_PLATFORM_VERSION,
Integer.valueOf(Build.VERSION.SDK_INT).toString());
}
serializer.startTag(null, TAG_ALL_GRANTS);
int numPkgs = pkgs.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState packageState = pkgs.get(i);
if (packageState != null) {
packageState.writeAsXml(serializer);
}
}
serializer.endTag(null, TAG_ALL_GRANTS);
serializer.endTag(null, TAG_PERMISSION_BACKUP);
serializer.endDocument();
}
/**
* Update the {@link Constants#DELAYED_RESTORE_PERMISSIONS_FILE} to contain the
* {@code packagesToRestoreLater}.
*
* @param packagesToRestoreLater The new pkgs in the delayed restore file
*/
private void writeDelayedStorePkgsLocked(
@NonNull ArrayList packagesToRestoreLater) {
try (OutputStream delayedRestoreData = mContext.openFileOutput(
DELAYED_RESTORE_PERMISSIONS_FILE, MODE_PRIVATE)) {
XmlSerializer serializer = newSerializer();
serializer.setOutput(delayedRestoreData, UTF_8.name());
writePkgsAsXml(serializer, packagesToRestoreLater);
serializer.flush();
} catch (IOException e) {
Log.e(LOG_TAG, "Could not remember which packages still need to be restored", e);
}
}
/**
* Write the state of all packages as XML.
*
* @param serializer The xml to write to
*/
void writeState(@NonNull XmlSerializer serializer) throws IOException {
List pkgs = mContext.getPackageManager().getInstalledPackages(
GET_PERMISSIONS);
ArrayList backupPkgs = new ArrayList<>();
int numPkgs = pkgs.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState packageState = BackupPackageState.fromAppPermissions(mContext,
pkgs.get(i));
if (packageState != null) {
backupPkgs.add(packageState);
}
}
writePkgsAsXml(serializer, backupPkgs);
}
/**
* Restore delayed permission state for a package (if delayed during {@link #restoreState}).
*
* @param packageName The package to be restored
*
* @return {@code true} if there is still delayed backup left
*/
boolean restoreDelayedState(@NonNull String packageName) {
synchronized (sLock) {
ArrayList packagesToRestoreLater;
try (FileInputStream delayedRestoreData =
mContext.openFileInput(DELAYED_RESTORE_PERMISSIONS_FILE)) {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(delayedRestoreData, UTF_8.name());
packagesToRestoreLater = parseFromXml(parser);
} catch (IOException | XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse delayed permissions", e);
return false;
}
PackageInfo pkgInfo = null;
try {
pkgInfo = mContext.getPackageManager().getPackageInfo(packageName, GET_PERMISSIONS);
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Could not restore delayed permissions for " + packageName, e);
}
if (pkgInfo != null) {
int numPkgs = packagesToRestoreLater.size();
for (int i = 0; i < numPkgs; i++) {
BackupPackageState pkgState = packagesToRestoreLater.get(i);
if (pkgState.mPackageName.equals(packageName)) {
pkgState.restore(mContext, pkgInfo);
packagesToRestoreLater.remove(i);
writeDelayedStorePkgsLocked(packagesToRestoreLater);
break;
}
}
}
return packagesToRestoreLater.size() > 0;
}
}
/**
* State that needs to be backed up for a permission.
*/
private static class BackupPermissionState {
private final @NonNull String mPermissionName;
private final boolean mIsGranted;
private final boolean mIsUserSet;
private final boolean mIsUserFixed;
private final boolean mWasReviewed;
private BackupPermissionState(@NonNull String permissionName, boolean isGranted,
boolean isUserSet, boolean isUserFixed, boolean wasReviewed) {
mPermissionName = permissionName;
mIsGranted = isGranted;
mIsUserSet = isUserSet;
mIsUserFixed = isUserFixed;
mWasReviewed = wasReviewed;
}
/**
* Parse a package state from XML.
*
* @param parser The data to read
* @param context a context to use
* @param backupPlatformVersion The platform version the backup was created on
*
* @return The state
*/
static @NonNull List parseFromXml(@NonNull XmlPullParser parser,
@NonNull Context context, int backupPlatformVersion)
throws XmlPullParserException {
String permName = parser.getAttributeValue(null, ATTR_PERMISSION_NAME);
if (permName == null) {
throw new XmlPullParserException("Found " + TAG_PERMISSION + " without "
+ ATTR_PERMISSION_NAME);
}
ArrayList expandedPermissions = new ArrayList<>();
expandedPermissions.add(permName);
List splitPerms = context.getSystemService(
PermissionManager.class).getSplitPermissions();
// Expand the properties to permissions that were split between the platform version the
// backup was taken and the current version.
int numSplitPerms = splitPerms.size();
for (int i = 0; i < numSplitPerms; i++) {
SplitPermissionInfo splitPerm = splitPerms.get(i);
if (backupPlatformVersion < splitPerm.getTargetSdk()
&& permName.equals(splitPerm.getSplitPermission())) {
expandedPermissions.addAll(splitPerm.getNewPermissions());
}
}
ArrayList parsedPermissions = new ArrayList<>(
expandedPermissions.size());
int numExpandedPerms = expandedPermissions.size();
for (int i = 0; i < numExpandedPerms; i++) {
parsedPermissions.add(new BackupPermissionState(expandedPermissions.get(i),
"true".equals(parser.getAttributeValue(null, ATTR_IS_GRANTED)),
"true".equals(parser.getAttributeValue(null, ATTR_USER_SET)),
"true".equals(parser.getAttributeValue(null, ATTR_USER_FIXED)),
"true".equals(parser.getAttributeValue(null, ATTR_WAS_REVIEWED))));
}
return parsedPermissions;
}
/**
* Is the permission granted, also considering the app-op.
*
* This does not consider the review-required state of the permission.
*
* @param perm The permission that might be granted
*
* @return {@code true} iff the permission and app-op is granted
*/
private static boolean isPermGrantedIncludingAppOp(@NonNull Permission perm) {
return perm.isGranted() && (!perm.affectsAppOp() || perm.isAppOpAllowed());
}
/**
* Get the state of a permission to back up.
*
* @param perm The permission to back up
* @param appSupportsRuntimePermissions If the app supports runtimePermissions
*
* @return The state to back up or {@code null} if the permission does not need to be
* backed up.
*/
private static @Nullable BackupPermissionState fromPermission(@NonNull Permission perm,
boolean appSupportsRuntimePermissions) {
int grantFlags = perm.getFlags();
if ((grantFlags & SYSTEM_RUNTIME_GRANT_MASK) != 0) {
return null;
}
if (!perm.isUserSet() && perm.isGrantedByDefault()) {
return null;
}
boolean permissionWasReviewed;
boolean isNotInDefaultGrantState;
if (appSupportsRuntimePermissions) {
isNotInDefaultGrantState = isPermGrantedIncludingAppOp(perm);
permissionWasReviewed = false;
} else {
isNotInDefaultGrantState = !isPermGrantedIncludingAppOp(perm);
permissionWasReviewed = !perm.isReviewRequired();
}
if (isNotInDefaultGrantState || perm.isUserSet() || perm.isUserFixed()
|| permissionWasReviewed) {
return new BackupPermissionState(perm.getName(), isPermGrantedIncludingAppOp(perm),
perm.isUserSet(), perm.isUserFixed(), permissionWasReviewed);
} else {
return null;
}
}
/**
* Get the states of all permissions of a group to back up.
*
* @param group The group of the permissions to back up
*
* @return The state to back up. Empty list if no permissions in the group need to be backed
* up
*/
static @NonNull ArrayList fromPermissionGroup(
@NonNull AppPermissionGroup group) {
ArrayList permissionsToRestore = new ArrayList<>();
List perms = group.getPermissions();
boolean appSupportsRuntimePermissions =
group.getApp().applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M;
int numPerms = perms.size();
for (int i = 0; i < numPerms; i++) {
BackupPermissionState permState = fromPermission(perms.get(i),
appSupportsRuntimePermissions);
if (permState != null) {
permissionsToRestore.add(permState);
}
}
return permissionsToRestore;
}
/**
* Write this state as XML.
*
* @param serializer The file to write to
*/
void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
serializer.startTag(null, TAG_PERMISSION);
serializer.attribute(null, ATTR_PERMISSION_NAME, mPermissionName);
if (mIsGranted) {
serializer.attribute(null, ATTR_IS_GRANTED, "true");
}
if (mIsUserSet) {
serializer.attribute(null, ATTR_USER_SET, "true");
}
if (mIsUserFixed) {
serializer.attribute(null, ATTR_USER_FIXED, "true");
}
if (mWasReviewed) {
serializer.attribute(null, ATTR_WAS_REVIEWED, "true");
}
serializer.endTag(null, TAG_PERMISSION);
}
/**
* Restore this permission state.
*
* @param appPerms The {@link AppPermissions} to restore the state to
* @param restoreBackgroundPerms if {@code true} only restore background permissions,
* if {@code false} do not restore background permissions
*/
void restore(@NonNull AppPermissions appPerms, boolean restoreBackgroundPerms) {
AppPermissionGroup group = appPerms.getGroupForPermission(mPermissionName);
if (group == null) {
Log.w(LOG_TAG, "Could not find group for " + mPermissionName + " in "
+ appPerms.getPackageInfo().packageName);
return;
}
if (restoreBackgroundPerms != group.isBackgroundGroup()) {
return;
}
Permission perm = group.getPermission(mPermissionName);
if (mWasReviewed) {
perm.unsetReviewRequired();
}
// Don't grant or revoke fixed permission groups
if (group.isSystemFixed() || group.isPolicyFixed()) {
return;
}
if (!perm.isUserSet()) {
if (mIsGranted) {
group.grantRuntimePermissions(false, mIsUserFixed,
new String[]{mPermissionName});
} else {
group.revokeRuntimePermissions(mIsUserFixed,
new String[]{mPermissionName});
}
perm.setUserSet(mIsUserSet);
}
}
}
/**
* State that needs to be backed up for a package.
*/
private static class BackupPackageState {
final @NonNull String mPackageName;
private final @NonNull ArrayList mPermissionsToRestore;
private BackupPackageState(@NonNull String packageName,
@NonNull ArrayList permissionsToRestore) {
mPackageName = packageName;
mPermissionsToRestore = permissionsToRestore;
}
/**
* Parse a package state from XML.
*
* @param parser The data to read
* @param context a context to use
* @param backupPlatformVersion The platform version the backup was created on
*
* @return The state
*/
static @NonNull BackupPackageState parseFromXml(@NonNull XmlPullParser parser,
@NonNull Context context, int backupPlatformVersion)
throws IOException, XmlPullParserException {
String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
if (packageName == null) {
throw new XmlPullParserException("Found " + TAG_GRANT + " without "
+ ATTR_PACKAGE_NAME);
}
ArrayList permissionsToRestore = new ArrayList<>();
while (true) {
switch (parser.next()) {
case START_TAG:
switch (parser.getName()) {
case TAG_PERMISSION:
try {
permissionsToRestore.addAll(
BackupPermissionState.parseFromXml(parser, context,
backupPlatformVersion));
} catch (XmlPullParserException e) {
Log.e(LOG_TAG, "Could not parse permission for "
+ packageName, e);
}
skipToEndOfTag(parser);
break;
default:
// ignore tag
Log.w(LOG_TAG, "Found unexpected tag " + parser.getName()
+ " while restoring " + packageName);
skipToEndOfTag(parser);
}
break;
case END_TAG:
return new BackupPackageState(packageName, permissionsToRestore);
case END_DOCUMENT:
throw new XmlPullParserException("Could not parse state for "
+ packageName);
}
}
}
/**
* Get the state of a package to back up.
*
* @param context A context to use
* @param pkgInfo The package to back up.
*
* @return The state to back up or {@code null} if no permission of the package need to be
* backed up.
*/
static @Nullable BackupPackageState fromAppPermissions(@NonNull Context context,
@NonNull PackageInfo pkgInfo) {
AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, null);
ArrayList permissionsToRestore = new ArrayList<>();
List groups = appPerms.getPermissionGroups();
int numGroups = groups.size();
for (int groupNum = 0; groupNum < numGroups; groupNum++) {
AppPermissionGroup group = groups.get(groupNum);
permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup(group));
// Background permissions are in a subgroup that is not part of
// {@link AppPermission#getPermissionGroups}. Hence add it explicitly here.
if (group.getBackgroundPermissions() != null) {
permissionsToRestore.addAll(BackupPermissionState.fromPermissionGroup(
group.getBackgroundPermissions()));
}
}
if (permissionsToRestore.size() == 0) {
return null;
}
return new BackupPackageState(pkgInfo.packageName, permissionsToRestore);
}
/**
* Write this state as XML.
*
* @param serializer The file to write to
*/
void writeAsXml(@NonNull XmlSerializer serializer) throws IOException {
if (mPermissionsToRestore.size() == 0) {
return;
}
serializer.startTag(null, TAG_GRANT);
serializer.attribute(null, ATTR_PACKAGE_NAME, mPackageName);
int numPerms = mPermissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).writeAsXml(serializer);
}
serializer.endTag(null, TAG_GRANT);
}
/**
* Restore this package state.
*
* @param context A context to use
* @param pkgInfo The package to restore.
*/
void restore(@NonNull Context context, @NonNull PackageInfo pkgInfo) {
AppPermissions appPerms = new AppPermissions(context, pkgInfo, false, true, null);
// Restore background permissions after foreground permissions as for pre-M apps bg
// granted and fg revoked cannot be expressed.
int numPerms = mPermissionsToRestore.size();
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).restore(appPerms, false);
}
for (int i = 0; i < numPerms; i++) {
mPermissionsToRestore.get(i).restore(appPerms, true);
}
int numGroups = appPerms.getPermissionGroups().size();
for (int i = 0; i < numGroups; i++) {
AppPermissionGroup group = appPerms.getPermissionGroups().get(i);
// Only denied groups can be user fixed
if (group.areRuntimePermissionsGranted()) {
group.setUserFixed(false);
}
AppPermissionGroup bgGroup = group.getBackgroundPermissions();
if (bgGroup != null) {
// Only denied groups can be user fixed
if (bgGroup.areRuntimePermissionsGranted()) {
bgGroup.setUserFixed(false);
}
}
}
appPerms.persistChanges(true);
}
}
}