Add callback and grant time manager
Bug: 260691599
Bug: 260884405
Test: installed/uninstalled packages with and without
shared user id and grant time state in logcat
Change-Id: I9f065b491988b0ac140b02fdd15014d9296bcfb2
diff --git a/service/java/com/android/server/healthconnect/HealthConnectManagerService.java b/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
index d89a792..61faecf 100644
--- a/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
+++ b/service/java/com/android/server/healthconnect/HealthConnectManagerService.java
@@ -20,6 +20,8 @@
import android.healthconnect.HealthConnectManager;
import com.android.server.SystemService;
+import com.android.server.healthconnect.permission.FirstGrantTimeDatastore;
+import com.android.server.healthconnect.permission.FirstGrantTimeManager;
import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
import com.android.server.healthconnect.permission.HealthPermissionIntentAppsTracker;
import com.android.server.healthconnect.permission.PackagePermissionChangesMonitor;
@@ -36,6 +38,7 @@
private final TransactionManager mTransactionManager;
private final HealthPermissionIntentAppsTracker mPermissionIntentTracker;
private final PackagePermissionChangesMonitor mPackageMonitor;
+ private final FirstGrantTimeManager mFirstGrantTimeManager;
public HealthConnectManagerService(Context context) {
super(context);
@@ -46,13 +49,21 @@
context.getPackageManager(),
HealthConnectManager.getHealthPermissions(context),
mPermissionIntentTracker);
- mPackageMonitor = new PackagePermissionChangesMonitor(mPermissionIntentTracker);
+ mFirstGrantTimeManager =
+ new FirstGrantTimeManager(
+ context,
+ mPermissionIntentTracker,
+ FirstGrantTimeDatastore.createInstance());
+ mPackageMonitor =
+ new PackagePermissionChangesMonitor(
+ mPermissionIntentTracker, mFirstGrantTimeManager);
mTransactionManager = TransactionManager.getInstance(getContext());
mContext = context;
}
@Override
public void onStart() {
+ mFirstGrantTimeManager.initializeState();
mPackageMonitor.registerBroadcastReceiver(mContext);
publishBinderService(
Context.HEALTHCONNECT_SERVICE,
diff --git a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastore.java b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastore.java
index a300049..26e3f28 100644
--- a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastore.java
+++ b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastore.java
@@ -19,16 +19,32 @@
import android.annotation.NonNull;
import android.os.UserHandle;
-/** Class for managing health permissions first grant time datastore. */
-interface FirstGrantTimeDatastore {
- /** Read {@link UserGrantTimeState for given user}. */
+/**
+ * Class for managing health permissions first grant time datastore.
+ *
+ * @hide
+ */
+public interface FirstGrantTimeDatastore {
+ /**
+ * Read {@link UserGrantTimeState for given user}.
+ *
+ * @hide
+ */
@NonNull
UserGrantTimeState readForUser(@NonNull UserHandle user);
- /** Write {@link UserGrantTimeState for given user}. */
+ /**
+ * Write {@link UserGrantTimeState for given user}.
+ *
+ * @hide
+ */
void writeForUser(@NonNull UserGrantTimeState grantTimesState, @NonNull UserHandle user);
- /** Create instance of the datastore class. */
+ /**
+ * Create instance of the datastore class.
+ *
+ * @hide
+ */
@NonNull
static FirstGrantTimeDatastore createInstance() {
return new FirstGrantTimeDatastoreXmlPersistence();
diff --git a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastoreXmlPersistence.java b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastoreXmlPersistence.java
index dfed48d..8f77dfa 100644
--- a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastoreXmlPersistence.java
+++ b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeDatastoreXmlPersistence.java
@@ -42,7 +42,7 @@
import java.util.Map;
class FirstGrantTimeDatastoreXmlPersistence implements FirstGrantTimeDatastore {
- private static final String TAG = "FirstGrantTimeDatastorePersistence";
+ private static final String TAG = "HealthConnectFirstGrantTimeDatastore";
private static final String GRANT_TIME_FILE_NAME = "health-permissions-first-grant-times.xml";
private static final String TAG_FIRST_GRANT_TIMES = "first-grant-times";
@@ -53,6 +53,11 @@
private static final String ATTRIBUTE_FIRST_GRANT_TIME = "first-grant-time";
private static final String ATTRIBUTE_VERSION = "version";
+ /**
+ * Read {@link UserGrantTimeState for given user}.
+ *
+ * @hide
+ */
@Nullable
@Override
public UserGrantTimeState readForUser(@NonNull UserHandle user) {
@@ -62,7 +67,7 @@
}
try (FileInputStream inputStream = new AtomicFile(file).openRead()) {
XmlPullParser parser = Xml.newPullParser();
- parser.setInput(inputStream, null);
+ parser.setInput(inputStream, /* inputEncoding= */ null);
return parseXml(parser);
} catch (FileNotFoundException e) {
Log.i(TAG, GRANT_TIME_FILE_NAME + " not found");
@@ -72,6 +77,11 @@
}
}
+ /**
+ * Write {@link UserGrantTimeState for given user}.
+ *
+ * @hide
+ */
@Override
public void writeForUser(
@NonNull UserGrantTimeState grantTimesState, @NonNull UserHandle user) {
@@ -105,19 +115,22 @@
private static void serializeGrantTimes(
@NonNull XmlSerializer serializer, @NonNull UserGrantTimeState userGrantTimeState)
throws IOException {
- serializer.startTag(null, TAG_FIRST_GRANT_TIMES);
+ serializer.startTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
serializer.attribute(
- null, ATTRIBUTE_VERSION, Integer.toString(userGrantTimeState.getVersion()));
+ /* namespace= */ null,
+ ATTRIBUTE_VERSION,
+ Integer.toString(userGrantTimeState.getVersion()));
for (Map.Entry<String, Instant> entry :
userGrantTimeState.getPackageGrantTimes().entrySet()) {
String packageName = entry.getKey();
Instant grantTime = entry.getValue();
- serializer.startTag(null, TAG_PACKAGE);
- serializer.attribute(null, ATTRIBUTE_NAME, packageName);
- serializer.attribute(null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
- serializer.endTag(null, TAG_PACKAGE);
+ serializer.startTag(/* namespace= */ null, TAG_PACKAGE);
+ serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, packageName);
+ serializer.attribute(
+ /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
+ serializer.endTag(/* namespace= */ null, TAG_PACKAGE);
}
for (Map.Entry<String, Instant> entry :
@@ -125,13 +138,14 @@
String sharedUserName = entry.getKey();
Instant grantTime = entry.getValue();
- serializer.startTag(null, TAG_SHARED_USER);
- serializer.attribute(null, ATTRIBUTE_NAME, sharedUserName);
- serializer.attribute(null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
- serializer.endTag(null, TAG_SHARED_USER);
+ serializer.startTag(/* namespace= */ null, TAG_SHARED_USER);
+ serializer.attribute(/* namespace= */ null, ATTRIBUTE_NAME, sharedUserName);
+ serializer.attribute(
+ /* namespace= */ null, ATTRIBUTE_FIRST_GRANT_TIME, grantTime.toString());
+ serializer.endTag(/* namespace= */ null, TAG_SHARED_USER);
}
- serializer.endTag(null, TAG_FIRST_GRANT_TIMES);
+ serializer.endTag(/* namespace= */ null, TAG_FIRST_GRANT_TIMES);
}
@NonNull
@@ -201,6 +215,10 @@
sharedUserPermissions.put(sharedUserName, firstGrantTime);
break;
}
+ default:
+ {
+ Log.w(TAG, "Tag " + parser.getName() + " is not parsed");
+ }
}
type = parser.next();
}
diff --git a/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java
new file mode 100644
index 0000000..e82a21f
--- /dev/null
+++ b/service/java/com/android/server/healthconnect/permission/FirstGrantTimeManager.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright (C) 2022 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.healthconnect.permission;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.healthconnect.Constants;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manager class of the health permissions first grant time.
+ *
+ * @hide
+ */
+public class FirstGrantTimeManager implements PackageManager.OnPermissionsChangedListener {
+ private static final String TAG = "HealthConnectFirstGrantTimeManager";
+ private static final int CURRENT_VERSION = 1;
+
+ private final PackageManager mPackageManager;
+ private final Context mContext;
+ private final HealthPermissionIntentAppsTracker mTracker;
+
+ private final Object mGrantTimeLock = new Object();
+
+ @GuardedBy("mGrantTimeLock")
+ private final FirstGrantTimeDatastore mDatastore;
+
+ @GuardedBy("mGrantTimeLock")
+ private final UidToGrantTimeCache mUidToGrantTimeCache;
+
+ private final PackageInfoUtils mPackageInfoHelper;
+
+ public FirstGrantTimeManager(
+ @NonNull Context context,
+ @NonNull HealthPermissionIntentAppsTracker tracker,
+ @NonNull FirstGrantTimeDatastore datastore) {
+ mContext = context;
+ mTracker = tracker;
+ mDatastore = datastore;
+ mPackageManager = context.getPackageManager();
+ mUidToGrantTimeCache = new UidToGrantTimeCache();
+ mPackageInfoHelper = new PackageInfoUtils(mContext);
+ }
+
+ /** Initialize first grant time state. */
+ public void initializeState() {
+
+ synchronized (mGrantTimeLock) {
+ Map<UserHandle, UserGrantTimeState> restoredState = restoreStatePerUserIfExists();
+
+ Map<UserHandle, List<PackageInfo>> validHealthApps =
+ mPackageInfoHelper.getPackagesHoldingHealthPermissions();
+ logIfInDebugMode("Packages holding health perms: ", validHealthApps);
+
+ // TODO(b/260585595): validate in B&R scenario.
+ validateAndCorrectRestoredState(restoredState, validHealthApps);
+
+ // TODO(b/260691599): consider removing mapping when getUidForSharedUser is visible
+ Map<String, Set<Integer>> sharedUserNamesToUid =
+ mPackageInfoHelper.collectSharedUserNameToUidsMapping(validHealthApps);
+ for (UserHandle user : restoredState.keySet()) {
+ mUidToGrantTimeCache.populateFromUserGrantTimeState(
+ restoredState.get(user), sharedUserNamesToUid, user);
+ }
+
+ mPackageManager.addOnPermissionsChangeListener(this);
+ logIfInDebugMode("State after init: ", restoredState);
+ logIfInDebugMode("Cache after init: ", mUidToGrantTimeCache);
+ }
+ }
+
+ @Override
+ public void onPermissionsChanged(int uid) {
+ String[] packageNames = mPackageManager.getPackagesForUid(uid);
+ if (packageNames == null) {
+ Log.w(TAG, "onPermissionsChanged: no known packages for UID: " + uid);
+ return;
+ }
+
+ UserHandle user = UserHandle.getUserHandleForUid(uid);
+ if (!checkSupportPermissionsUsageIntent(packageNames, user)) {
+ logIfInDebugMode("Can find health intent declaration in ", packageNames[0]);
+ return;
+ }
+
+ boolean anyHealthPermissionGranted =
+ mPackageInfoHelper.hasGrantedHealthPermissions(packageNames, user);
+
+ synchronized (mGrantTimeLock) {
+ boolean grantTimeRecorded = mUidToGrantTimeCache.containsKey(uid);
+
+ if (grantTimeRecorded != anyHealthPermissionGranted) {
+ if (grantTimeRecorded) {
+ // An app doesn't have health permissions anymore, reset its grant time.
+ mUidToGrantTimeCache.remove(uid);
+ } else {
+ // An app got new health permission, set current time as it's first grant
+ // time.
+ mUidToGrantTimeCache.put(uid, Instant.now());
+ }
+
+ UserGrantTimeState updatedState =
+ mUidToGrantTimeCache.extractUserGrantTimeState(user);
+ logIfInDebugMode("State after onPermissionsChanged :", updatedState);
+ mDatastore.writeForUser(updatedState, user);
+ }
+ }
+ }
+
+ void onPackageRemoved(
+ @NonNull String packageName, int removedPackageUid, @NonNull UserHandle userHandle) {
+ String[] leftSharedUidPackages =
+ mPackageInfoHelper.getPackagesForUid(removedPackageUid, userHandle);
+ if (leftSharedUidPackages != null && leftSharedUidPackages.length > 0) {
+ // There are installed packages left with given UID,
+ // don't need to update grant time state.
+ return;
+ }
+
+ synchronized (mGrantTimeLock) {
+ if (mUidToGrantTimeCache.containsKey(removedPackageUid)) {
+ mUidToGrantTimeCache.remove(removedPackageUid);
+ UserGrantTimeState updatedState =
+ mUidToGrantTimeCache.extractUserGrantTimeState(userHandle);
+ logIfInDebugMode("State after package " + packageName + " removed: ", updatedState);
+ mDatastore.writeForUser(updatedState, userHandle);
+ }
+ }
+ }
+
+ @GuardedBy("mGrantTimeLock")
+ private Map<UserHandle, UserGrantTimeState> restoreStatePerUserIfExists() {
+ List<UserHandle> userHandles =
+ mContext.getSystemService(UserManager.class)
+ .getUserHandles(/* exclude dying= */ true);
+ Map<UserHandle, UserGrantTimeState> userToUserGrantTimeState = new ArrayMap<>();
+ for (UserHandle userHandle : userHandles) {
+ try {
+ UserGrantTimeState restoredState = mDatastore.readForUser(userHandle);
+ if (restoredState == null) {
+ userToUserGrantTimeState.put(
+ userHandle, new UserGrantTimeState(CURRENT_VERSION));
+ } else {
+ userToUserGrantTimeState.put(userHandle, restoredState);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error while reading from datastore: " + e);
+ }
+ }
+ return userToUserGrantTimeState;
+ }
+
+ /**
+ * Validate current state and remove apps which are not present / hold health permissions, set
+ * new grant time to apps which doesn't have grant time but installed and hold health
+ * permissions. It should mitigate situation e.g. when permission mainline module did roll-back
+ * and some health permissions got granted/revoked without onPermissionsChanged callback.
+ *
+ * @param userToHealthPackagesInfos mapping of UserHandle to List of PackagesInfo of apps which
+ * currently hold health permissions.
+ */
+ @GuardedBy("mGrantTimeLock")
+ private void validateAndCorrectRestoredState(
+ Map<UserHandle, UserGrantTimeState> recordedStatePerUser,
+ Map<UserHandle, List<PackageInfo>> userToHealthPackagesInfos) {
+ for (UserHandle user : userToHealthPackagesInfos.keySet()) {
+ validateAndCorrectRecordedStateForUser(
+ recordedStatePerUser.get(user), userToHealthPackagesInfos.get(user), user);
+ }
+ }
+
+ @GuardedBy("mGrantTimeLock")
+ private void validateAndCorrectRecordedStateForUser(
+ @NonNull UserGrantTimeState recordedState,
+ @NonNull List<PackageInfo> healthPackagesInfos,
+ @NonNull UserHandle user) {
+ Set<String> validPackagesPerUser = new ArraySet<>();
+ Set<String> validSharedUsersPerUser = new ArraySet<>();
+
+ boolean stateChanged = false;
+ logIfInDebugMode("Valid apps for " + user + ": ", healthPackagesInfos);
+
+ // If package holds health permissions but doesn't have recorded grant
+ // time (e.g. because of permissions rollback), set current time as the first grant time.
+ for (PackageInfo info : healthPackagesInfos) {
+ if (info.sharedUserId == null) {
+ stateChanged |= setPackageGrantTimeIfNotRecorded(recordedState, info.packageName);
+ validPackagesPerUser.add(info.packageName);
+ } else {
+ stateChanged |=
+ setSharedUserGrantTimeIfNotRecorded(recordedState, info.sharedUserId);
+ validSharedUsersPerUser.add(info.sharedUserId);
+ }
+ }
+
+ // If package is not installed / doesn't hold health permissions
+ // but has recorded first grant time, remove it from grant time state.
+ stateChanged |=
+ removeInvalidPackagesFromGrantTimeStateForUser(recordedState, validPackagesPerUser);
+
+ stateChanged |=
+ removeInvalidSharedUsersFromGrantTimeStateForUser(
+ recordedState, validSharedUsersPerUser);
+
+ if (stateChanged) {
+ logIfInDebugMode("Changed state after validation for " + user + ": ", recordedState);
+ mDatastore.writeForUser(recordedState, user);
+ }
+ }
+
+ private boolean setPackageGrantTimeIfNotRecorded(
+ @NonNull UserGrantTimeState grantTimeState, @NonNull String packageName) {
+ if (!grantTimeState.containsPackageGrantTime(packageName)) {
+ Log.w(
+ TAG,
+ "No recorded grant time for package:"
+ + packageName
+ + ". Assigning current time as the first grant time.");
+ grantTimeState.setPackageGrantTime(packageName, Instant.now());
+ return true;
+ }
+ return false;
+ }
+
+ private boolean setSharedUserGrantTimeIfNotRecorded(
+ @NonNull UserGrantTimeState grantTimeState, @NonNull String sharedUserIdName) {
+ if (!grantTimeState.containsSharedUserGrantTime(sharedUserIdName)) {
+ Log.w(
+ TAG,
+ "No recorded grant time for shared user:"
+ + sharedUserIdName
+ + ". Assigning current time as first grant time.");
+ grantTimeState.setSharedUserGrantTime(sharedUserIdName, Instant.now());
+ return true;
+ }
+ return false;
+ }
+
+ private boolean removeInvalidPackagesFromGrantTimeStateForUser(
+ @NonNull UserGrantTimeState recordedState, @NonNull Set<String> validApps) {
+ Set<String> recordedButNotValid =
+ new ArraySet<>(recordedState.getPackageGrantTimes().keySet());
+ if (validApps != null) {
+ recordedButNotValid.removeAll(validApps);
+ }
+
+ if (!recordedButNotValid.isEmpty()) {
+ Log.w(
+ TAG,
+ "Packages "
+ + recordedButNotValid
+ + " have recorded grant times, but not installed or hold health "
+ + "permissions anymore. Removing them from the grant time state.");
+ recordedState.getPackageGrantTimes().keySet().removeAll(recordedButNotValid);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean removeInvalidSharedUsersFromGrantTimeStateForUser(
+ @NonNull UserGrantTimeState recordedState, @NonNull Set<String> validSharedUsers) {
+ Set<String> recordedButNotValid =
+ new ArraySet<>(recordedState.getSharedUserGrantTimes().keySet());
+ if (validSharedUsers != null) {
+ recordedButNotValid.removeAll(validSharedUsers);
+ }
+
+ if (!recordedButNotValid.isEmpty()) {
+ Log.w(
+ TAG,
+ "Shared users "
+ + recordedButNotValid
+ + " have recorded grant times, but not installed or hold health "
+ + "permissions anymore. Removing them from the grant time state.");
+ recordedState.getSharedUserGrantTimes().keySet().removeAll(recordedButNotValid);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean checkSupportPermissionsUsageIntent(
+ @NonNull String[] names, @NonNull UserHandle user) {
+ for (String packageName : names) {
+ if (mTracker.supportsPermissionUsageIntent(packageName, user)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void logIfInDebugMode(@NonNull String prefixMessage, @NonNull Object objectToLog) {
+ if (Constants.DEBUG) {
+ Log.d(TAG, prefixMessage + objectToLog.toString());
+ }
+ }
+
+ private class UidToGrantTimeCache {
+ private final Map<Integer, Instant> mUidToGrantTime;
+
+ UidToGrantTimeCache() {
+ mUidToGrantTime = new ArrayMap<>();
+ }
+
+ @Nullable
+ Instant remove(@Nullable Integer uid) {
+ if (uid == null) {
+ return null;
+ }
+ return mUidToGrantTime.remove(uid);
+ }
+
+ boolean containsKey(@Nullable Integer uid) {
+ if (uid == null) {
+ return false;
+ }
+ return mUidToGrantTime.containsKey(uid);
+ }
+
+ @Nullable
+ Instant put(@NonNull Integer uid, @NonNull Instant time) {
+ return mUidToGrantTime.put(uid, time);
+ }
+
+ @Override
+ public String toString() {
+ return mUidToGrantTime.toString();
+ }
+
+ @NonNull
+ UserGrantTimeState extractUserGrantTimeState(@NonNull UserHandle user) {
+ Map<String, Instant> sharedUserToGrantTime = new ArrayMap<>();
+ Map<String, Instant> packageNameToGrantTime = new ArrayMap<>();
+
+ for (Map.Entry<Integer, Instant> entry : mUidToGrantTime.entrySet()) {
+ Integer uid = entry.getKey();
+ Instant time = entry.getValue();
+
+ if (!UserHandle.getUserHandleForUid(uid).equals(user)) {
+ continue;
+ }
+
+ String sharedUserName = mPackageInfoHelper.getSharedUserNameFromUid(uid);
+ if (sharedUserName != null) {
+ sharedUserToGrantTime.put(sharedUserName, time);
+ } else {
+ String packageName = mPackageInfoHelper.getPackageNameFromUid(uid);
+ if (packageName != null) {
+ packageNameToGrantTime.put(packageName, time);
+ }
+ }
+ }
+
+ return new UserGrantTimeState(
+ packageNameToGrantTime, sharedUserToGrantTime, CURRENT_VERSION);
+ }
+
+ void populateFromUserGrantTimeState(
+ @NonNull UserGrantTimeState grantTimeState,
+ @NonNull Map<String, Set<Integer>> sharedUserNameToUids,
+ @NonNull UserHandle user) {
+ for (Map.Entry<String, Instant> entry :
+ grantTimeState.getSharedUserGrantTimes().entrySet()) {
+ String sharedUserName = entry.getKey();
+ Instant time = entry.getValue();
+
+ if (sharedUserNameToUids.get(sharedUserName) == null) {
+ continue;
+ }
+
+ for (Integer uid : sharedUserNameToUids.get(sharedUserName)) {
+ put(uid, time);
+ }
+ }
+
+ for (Map.Entry<String, Instant> entry :
+ grantTimeState.getPackageGrantTimes().entrySet()) {
+ String packageName = entry.getKey();
+ Instant time = entry.getValue();
+
+ Integer uid = mPackageInfoHelper.getPackageUid(packageName, user);
+ if (uid != null) {
+ put(uid, time);
+ }
+ }
+ }
+ }
+}
diff --git a/service/java/com/android/server/healthconnect/permission/HealthPermissionIntentAppsTracker.java b/service/java/com/android/server/healthconnect/permission/HealthPermissionIntentAppsTracker.java
index 38a10b6..1d14dc6 100644
--- a/service/java/com/android/server/healthconnect/permission/HealthPermissionIntentAppsTracker.java
+++ b/service/java/com/android/server/healthconnect/permission/HealthPermissionIntentAppsTracker.java
@@ -77,6 +77,10 @@
return false;
}
+ if (!mUserToHealthPackageNamesMap.get(userHandle).contains(packageName)) {
+ updatePackageStateForUser(packageName, userHandle);
+ }
+
return mUserToHealthPackageNamesMap.get(userHandle).contains(packageName);
}
}
diff --git a/service/java/com/android/server/healthconnect/permission/PackageInfoUtils.java b/service/java/com/android/server/healthconnect/permission/PackageInfoUtils.java
new file mode 100644
index 0000000..ef56906
--- /dev/null
+++ b/service/java/com/android/server/healthconnect/permission/PackageInfoUtils.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 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.healthconnect.permission;
+
+import static android.content.pm.PackageManager.GET_PERMISSIONS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.healthconnect.HealthConnectManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Utility class with PackageInfo-related methods for {@link FirstGrantTimeManager} */
+class PackageInfoUtils {
+ private static final String TAG = "HealthConnectPackageInfoUtils";
+
+ /**
+ * Store PackageManager for each user. Keys are users, values are PackageManagers which get from
+ * each user.
+ */
+ private final Map<UserHandle, PackageManager> mUsersPackageManager = new ArrayMap<>();
+
+ private final Context mContext;
+
+ PackageInfoUtils(Context context) {
+ mContext = context;
+ }
+
+ @NonNull
+ Map<String, Set<Integer>> collectSharedUserNameToUidsMapping(
+ @NonNull Map<UserHandle, List<PackageInfo>> userHandleToPackageInfo) {
+ Map<String, Set<Integer>> sharedUserNameToUids = new ArrayMap<>();
+ for (List<PackageInfo> healthPackagesInfos : userHandleToPackageInfo.values()) {
+ for (PackageInfo info : healthPackagesInfos) {
+ if (info.sharedUserId != null) {
+ if (sharedUserNameToUids.get(info.sharedUserId) == null) {
+ sharedUserNameToUids.put(info.sharedUserId, new ArraySet<>());
+ }
+ sharedUserNameToUids.get(info.sharedUserId).add(info.applicationInfo.uid);
+ }
+ }
+ }
+ return sharedUserNameToUids;
+ }
+
+ @NonNull
+ Map<UserHandle, List<PackageInfo>> getPackagesHoldingHealthPermissions() {
+ // TODO(b/260707328): replace with getPackagesHoldingPermissions
+ Map<UserHandle, List<PackageInfo>> userToHealthAppsInfo = new ArrayMap<>();
+ for (UserHandle user : getAllUserHandles()) {
+ List<PackageInfo> allInfos =
+ getPackageManagerAsUser(user)
+ .getInstalledPackages(
+ PackageManager.PackageInfoFlags.of(GET_PERMISSIONS));
+ List<PackageInfo> healthAppsInfos = new ArrayList<>();
+
+ for (PackageInfo info : allInfos) {
+ if (anyRequestedHealthPermissionGranted(info)) {
+ healthAppsInfos.add(info);
+ }
+ }
+ userToHealthAppsInfo.put(user, healthAppsInfos);
+ }
+
+ return userToHealthAppsInfo;
+ }
+
+ boolean hasGrantedHealthPermissions(@NonNull String[] packageNames, @NonNull UserHandle user) {
+ for (String packageName : packageNames) {
+ PackageInfo info = getPackageInfoWithPermissionsAsUser(packageName, user);
+ if (anyRequestedHealthPermissionGranted(info)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ String[] getPackagesForUid(@NonNull int packageUid, @NonNull UserHandle user) {
+ return getPackageManagerAsUser(user).getPackagesForUid(packageUid);
+ }
+
+ private boolean anyRequestedHealthPermissionGranted(@NonNull PackageInfo packageInfo) {
+ if (packageInfo == null || packageInfo.requestedPermissions == null) {
+ Log.w(TAG, "Can't extract requested permissions from the package info.");
+ return false;
+ }
+
+ for (int i = 0; i < packageInfo.requestedPermissions.length; i++) {
+ String currPerm = packageInfo.requestedPermissions[i];
+ if (HealthConnectManager.isHealthPermission(mContext, currPerm)
+ && ((packageInfo.requestedPermissionsFlags[i]
+ & PackageInfo.REQUESTED_PERMISSION_GRANTED)
+ != 0)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ PackageInfo getPackageInfoWithPermissionsAsUser(
+ @NonNull String packageName, @NonNull UserHandle user) {
+ try {
+ return getPackageManagerAsUser(user)
+ .getPackageInfo(
+ packageName, PackageManager.PackageInfoFlags.of(GET_PERMISSIONS));
+ } catch (PackageManager.NameNotFoundException e) {
+ // App not found.
+ Log.e(TAG, "NameNotFoundException for " + packageName);
+ return null;
+ }
+ }
+
+ @Nullable
+ String getSharedUserNameFromUid(int uid) {
+ String[] packages =
+ mUsersPackageManager
+ .get(UserHandle.getUserHandleForUid(uid))
+ .getPackagesForUid(uid);
+ if (packages == null || packages.length == 0) {
+ Log.e(TAG, "Can't get package names for UID: " + uid);
+ return null;
+ }
+ try {
+ PackageInfo info =
+ getPackageManagerAsUser(UserHandle.getUserHandleForUid(uid))
+ .getPackageInfo(packages[0], PackageManager.PackageInfoFlags.of(0));
+ return info.sharedUserId;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Package " + packages[0] + " not found.");
+ return null;
+ }
+ }
+
+ @Nullable
+ String getPackageNameFromUid(int uid) {
+ String[] packages =
+ mUsersPackageManager
+ .get(UserHandle.getUserHandleForUid(uid))
+ .getPackagesForUid(uid);
+ if (packages == null || packages.length != 1) {
+ Log.w(TAG, "Can't get one package name for UID: " + uid);
+ return null;
+ }
+ try {
+ PackageInfo info =
+ getPackageManagerAsUser(UserHandle.getUserHandleForUid(uid))
+ .getPackageInfo(packages[0], PackageManager.PackageInfoFlags.of(0));
+ return info.packageName;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Package " + packages[0] + " not found.");
+ return null;
+ }
+ }
+
+ @Nullable
+ Integer getPackageUid(@NonNull String packageName, @NonNull UserHandle user) {
+ Integer uid = null;
+ try {
+ uid =
+ getPackageManagerAsUser(user)
+ .getPackageUid(
+ packageName,
+ PackageManager.PackageInfoFlags.of(/* flags= */ 0));
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "NameNotFound exception for " + packageName);
+ }
+ return uid;
+ }
+
+ @NonNull
+ private PackageManager getPackageManagerAsUser(@NonNull UserHandle user) {
+ PackageManager packageManager = mUsersPackageManager.get(user);
+ if (packageManager == null) {
+ packageManager = mContext.createContextAsUser(user, /* flag= */ 0).getPackageManager();
+ mUsersPackageManager.put(user, packageManager);
+ }
+ return packageManager;
+ }
+
+ @NonNull
+ private List<UserHandle> getAllUserHandles() {
+ return Objects.requireNonNull(
+ mContext.getSystemService(UserManager.class),
+ "UserManager service cannot be null")
+ .getUserHandles(/* excludeDying= */ true);
+ }
+}
diff --git a/service/java/com/android/server/healthconnect/permission/PackagePermissionChangesMonitor.java b/service/java/com/android/server/healthconnect/permission/PackagePermissionChangesMonitor.java
index e853bb6..ce31214 100644
--- a/service/java/com/android/server/healthconnect/permission/PackagePermissionChangesMonitor.java
+++ b/service/java/com/android/server/healthconnect/permission/PackagePermissionChangesMonitor.java
@@ -35,10 +35,13 @@
private static final String TAG = "HealthPackageChangesMonitor";
static final IntentFilter sPackageFilter = buildPackageChangeFilter();
private final HealthPermissionIntentAppsTracker mPermissionIntentTracker;
+ private final FirstGrantTimeManager mFirstGrantTimeManager;
public PackagePermissionChangesMonitor(
- HealthPermissionIntentAppsTracker permissionIntentTracker) {
+ HealthPermissionIntentAppsTracker permissionIntentTracker,
+ FirstGrantTimeManager grantTimeManager) {
mPermissionIntentTracker = permissionIntentTracker;
+ mFirstGrantTimeManager = grantTimeManager;
}
/**
@@ -60,6 +63,12 @@
return;
}
mPermissionIntentTracker.onPackageChanged(packageName, userHandle);
+
+ if (intent.getAction().equals(Intent.ACTION_PACKAGE_REMOVED)
+ && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+ final int uid = intent.getIntExtra(Intent.EXTRA_UID, /* default value= */ -1);
+ mFirstGrantTimeManager.onPackageRemoved(packageName, uid, userHandle);
+ }
}
private static IntentFilter buildPackageChangeFilter() {
diff --git a/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java b/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
index b3798b1..3259cc9 100644
--- a/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
+++ b/service/java/com/android/server/healthconnect/permission/UserGrantTimeState.java
@@ -17,12 +17,17 @@
package com.android.server.healthconnect.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
import java.time.Instant;
import java.util.Map;
-import java.util.Objects;
-/** State of user health permissions first grant times. Used by {@link FirstGrantTimeDatastore}. */
+/**
+ * State of user health permissions first grant times. Used by {@link FirstGrantTimeDatastore}.
+ *
+ * @hide
+ */
class UserGrantTimeState {
/** Special value for {@link #mVersion} to indicate that no version was read. */
public static final int NO_VERSION = -1;
@@ -36,6 +41,10 @@
/** The version of the grant times state. */
private final int mVersion;
+ UserGrantTimeState(@NonNull int version) {
+ this(new ArrayMap<>(), new ArrayMap<>(), version);
+ }
+
UserGrantTimeState(
@NonNull Map<String, Instant> packagePermissions,
@NonNull Map<String, Instant> sharedUserPermissions,
@@ -55,27 +64,39 @@
return mSharedUserPermissions;
}
+ void setPackageGrantTime(@NonNull String packageName, @Nullable Instant time) {
+ mPackagePermissions.put(packageName, time);
+ }
+
+ void setSharedUserGrantTime(@NonNull String sharedUserId, @Nullable Instant time) {
+ mSharedUserPermissions.put(sharedUserId, time);
+ }
+
+ boolean containsPackageGrantTime(@NonNull String packageName) {
+ return mPackagePermissions.containsKey(packageName);
+ }
+
+ boolean containsSharedUserGrantTime(@NonNull String sharedUserId) {
+ return mSharedUserPermissions.containsKey(sharedUserId);
+ }
+
+ /**
+ * Get the version of the grant time.
+ *
+ * @return the version of the grant time
+ */
int getVersion() {
return mVersion;
}
@Override
- public boolean equals(Object object) {
- if (this == object) {
- return true;
- }
- if (!(object instanceof UserGrantTimeState)) {
- return false;
- }
-
- UserGrantTimeState that = (UserGrantTimeState) object;
- return Objects.equals(mPackagePermissions, that.mPackagePermissions)
- && Objects.equals(mSharedUserPermissions, that.mSharedUserPermissions)
- && (mVersion == that.mVersion);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mPackagePermissions, mSharedUserPermissions, mVersion);
+ public String toString() {
+ return "GrantTimeState{version="
+ + mVersion
+ + ",packagePermissions="
+ + mPackagePermissions.toString()
+ + ",sharedUserPermissions="
+ + mSharedUserPermissions.toString()
+ + "}";
}
}
diff --git a/tests/unittests/src/com/android/server/healthconnect/permission/GrantTimePersistenceUnitTest.java b/tests/unittests/src/com/android/server/healthconnect/permission/GrantTimePersistenceUnitTest.java
index 7c26116..9347b9b 100644
--- a/tests/unittests/src/com/android/server/healthconnect/permission/GrantTimePersistenceUnitTest.java
+++ b/tests/unittests/src/com/android/server/healthconnect/permission/GrantTimePersistenceUnitTest.java
@@ -42,8 +42,6 @@
@RunWith(AndroidJUnit4.class)
public class GrantTimePersistenceUnitTest {
- private MockitoSession mStaticMockSession;
-
private static final UserGrantTimeState DEFAULT_STATE =
new UserGrantTimeState(
Map.of("package1", Instant.ofEpochSecond((long) 1e8)),
@@ -73,6 +71,7 @@
private static final UserGrantTimeState EMPTY_STATE =
new UserGrantTimeState(new ArrayMap<>(), new ArrayMap<>(), 3);
+ private MockitoSession mStaticMockSession;
private UserHandle mUser = UserHandle.of(UserHandle.myUserId());
private File mMockDataDirectory;
@@ -96,7 +95,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_defaultState() {
+ public void testWriteReadData_packageAndSharedUserState_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(DEFAULT_STATE, mUser);
UserGrantTimeState restoredState = datastore.readForUser(mUser);
@@ -104,7 +103,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_sharedUsersState() {
+ public void testWriteReadData_multipleSharedUserState_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(SHARED_USERS_STATE, mUser);
UserGrantTimeState restoredState = datastore.readForUser(mUser);
@@ -112,7 +111,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_packagesState() {
+ public void testWriteReadData_multiplePackagesState_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(PACKAGES_STATE, mUser);
UserGrantTimeState restoredState = datastore.readForUser(mUser);
@@ -120,7 +119,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_emptyState() {
+ public void testWriteReadData_emptyState_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(EMPTY_STATE, mUser);
UserGrantTimeState restoredState = datastore.readForUser(mUser);
@@ -128,7 +127,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_afterOverwriting() {
+ public void testWriteReadData_overwroteState_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(PACKAGES_STATE, mUser);
datastore.writeForUser(DEFAULT_STATE, mUser);
@@ -137,7 +136,7 @@
}
@Test
- public void testWriteReadDatabase_RestoredStateIsEqualToWritten_forDifferentUsers() {
+ public void testWriteReadData_statesForTwoUsersWritten_restoredCorrectly() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
datastore.writeForUser(PACKAGES_STATE, mUser);
datastore.writeForUser(SHARED_USERS_STATE, UserHandle.of(10));
@@ -148,13 +147,13 @@
}
@Test
- public void testReadDatabase_StateIsNotWritten_nullIsReturned() {
+ public void testReadData_stateIsNotWritten_nullIsReturned() {
FirstGrantTimeDatastore datastore = FirstGrantTimeDatastore.createInstance();
UserGrantTimeState state = datastore.readForUser(mUser);
assertThat(state).isNull();
}
- private void deleteFile(File file) {
+ private static void deleteFile(File file) {
File[] contents = file.listFiles();
if (contents != null) {
for (File f : contents) {
@@ -164,9 +163,8 @@
assertThat(file.delete()).isTrue();
}
- void assertRestoredStateIsCorrect(
+ private static void assertRestoredStateIsCorrect(
UserGrantTimeState restoredState, UserGrantTimeState initialState) {
- assertThat(initialState).isEqualTo(restoredState);
assertThat(initialState.getVersion()).isEqualTo(restoredState.getVersion());
assertThat(initialState.getPackageGrantTimes())
.isEqualTo(restoredState.getPackageGrantTimes());