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());