• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.healthconnect.storage.datatypehelpers;
18 
19 import static android.app.AppOpsManager.HISTORY_FLAG_GET_ATTRIBUTION_CHAINS;
20 import static android.app.AppOpsManager.OP_FLAG_SELF;
21 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED;
22 import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXY;
23 
24 import android.annotation.NonNull;
25 import android.app.AppOpsManager;
26 import android.app.AppOpsManager.AttributedOpEntry;
27 import android.app.AppOpsManager.HistoricalOp;
28 import android.app.AppOpsManager.HistoricalOps;
29 import android.app.AppOpsManager.HistoricalOpsRequest;
30 import android.app.AppOpsManager.HistoricalPackageOps;
31 import android.app.AppOpsManager.HistoricalUidOps;
32 import android.content.pm.PackageManager;
33 import android.health.connect.accesslog.AccessLog;
34 import android.health.connect.datatypes.RecordTypeIdentifier;
35 import android.os.UserHandle;
36 import android.util.Slog;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import java.time.Instant;
41 import java.util.concurrent.CountDownLatch;
42 import java.util.concurrent.LinkedBlockingQueue;
43 import java.util.concurrent.TimeUnit;
44 import java.util.concurrent.atomic.AtomicReference;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.List;
48 import java.util.Set;
49 import java.util.stream.Collectors;
50 
51 /**
52  * A helper class to fetch and store the access logs from AppOps.
53  *
54  * @hide
55  */
56 public final class AppOpLogsHelper {
57     private static final String TAG = "AppOpLogsHelper";
58     private static final int AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 = 1;
59     private static final int AGGREGATE_DATA_FILTER_BEGIN_DAYS_7 = 7;
60 
61     private final AppOpsManager mAppOpsManager;
62     private final PackageManager mPackageManager;
63     private final Set<String> mGranularHealthAppOps;
64 
AppOpLogsHelper( AppOpsManager appOpsManager, PackageManager packageManager, Set<String> healthPermissions)65     public AppOpLogsHelper(
66             AppOpsManager appOpsManager,
67             PackageManager packageManager,
68             Set<String> healthPermissions) {
69         mAppOpsManager = appOpsManager;
70         mPackageManager = packageManager;
71         mGranularHealthAppOps =
72                 healthPermissions.stream()
73                         .filter(
74                                 permissionName -> {
75                                     String appOp = AppOpsManager.permissionToOp(permissionName);
76                                     // AppOp may be null if the permission is introduced but
77                                     // disabled (i.e. READ_ACTIVITY_INTENSITY).
78                                     return appOp != null
79                                             && !appOp.equals(
80                                                     AppOpsManager.OPSTR_READ_WRITE_HEALTH_DATA);
81                                 })
82                         .map(AppOpsManager::permissionToOp)
83                         .collect(Collectors.toSet());
84     }
85 
86     /**
87      * Reads from AppOps to fetch historical usage of health permissions.
88      *
89      * @return a list of {@link AccessLog} converted from AppOps historical usages.
90      */
getAccessLogsFromAppOps(UserHandle callingUserHandle)91     public List<AccessLog> getAccessLogsFromAppOps(UserHandle callingUserHandle) {
92         if (mGranularHealthAppOps.isEmpty()) {
93             return new ArrayList<>();
94         }
95 
96         // Create request.
97 
98         // Begin / End times are kept in sync with PermissionController.
99         int aggregateDataFilterBeginDays =
100                 isWatch() ? AGGREGATE_DATA_FILTER_BEGIN_DAYS_1 : AGGREGATE_DATA_FILTER_BEGIN_DAYS_7;
101         long beginTimeMillis =
102                 Math.max(
103                         System.currentTimeMillis()
104                                 - TimeUnit.DAYS.toMillis(aggregateDataFilterBeginDays),
105                         Instant.EPOCH.toEpochMilli());
106         long endTimeMillis = Long.MAX_VALUE;
107         List<String> opNamesToQuery = new ArrayList<>(mGranularHealthAppOps);
108         final AppOpsManager.HistoricalOpsRequest request =
109                 new HistoricalOpsRequest.Builder(beginTimeMillis, endTimeMillis)
110                         .setOpNames(opNamesToQuery)
111                         .setFlags(AppOpsManager.OP_FLAG_SELF
112                             | AppOpsManager.OP_FLAG_TRUSTED_PROXIED
113                             | OP_FLAG_TRUSTED_PROXY)
114                         .setHistoryFlags(
115                                 AppOpsManager.HISTORY_FLAG_DISCRETE
116                                         | HISTORY_FLAG_GET_ATTRIBUTION_CHAINS)
117                         .build();
118         // Get Historical Ops.
119         final LinkedBlockingQueue<HistoricalOps> historicalOpsQueue = new LinkedBlockingQueue<>(1);
120         mAppOpsManager.getHistoricalOps(
121                 request,
122                 Runnable::run,
123                 (HistoricalOps ops) -> {
124                     if (!historicalOpsQueue.offer(ops)) {
125                         Slog.e(TAG, "Failed to put historical ops into queue");
126                     }
127                 });
128         HistoricalOps histOps;
129         try {
130             // TODO: b/364643016 - This seems like a long delay?
131             // TODO: b/364643016 - Explore moving this to an async call.
132             histOps = historicalOpsQueue.poll(30, TimeUnit.SECONDS);
133             if (histOps == null) {
134                 Slog.e(TAG, "Historical ops query timed out");
135                 return new ArrayList<>();
136             }
137         } catch (InterruptedException ignored) {
138             Slog.e(TAG, "Historical ops query interrupted");
139             Thread.currentThread().interrupt(); // Restore the interrupted status
140             return new ArrayList<>();
141         }
142 
143         List<AccessLog> logs = new ArrayList<>();
144         // Generate HealthConnect access logs from AppOps.
145         for (int uidIdx = 0; uidIdx < histOps.getUidCount(); uidIdx++) {
146             HistoricalUidOps uidOps = histOps.getUidOpsAt(uidIdx);
147 
148             // Filter out any app ops from a different user.
149             UserHandle opsUserHandle = UserHandle.getUserHandleForUid(uidOps.getUid());
150             if (!opsUserHandle.equals(callingUserHandle)) {
151                 continue;
152             }
153 
154             for (int pkgIdx = 0; pkgIdx < uidOps.getPackageCount(); pkgIdx++) {
155                 final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(pkgIdx);
156                 String packageName = packageOps.getPackageName();
157                 for (String opName : mGranularHealthAppOps) {
158                     HistoricalOp historicalOp = packageOps.getOp(opName);
159                     if (historicalOp == null) {
160                         continue;
161                     }
162                     int recordType = opNameToRecordType(opName);
163                     if (recordType == RecordTypeIdentifier.RECORD_TYPE_UNKNOWN) {
164                         continue;
165                     }
166                     @RecordTypeIdentifier.RecordType
167                     List<Integer> recordTypes = Arrays.asList(recordType);
168                     for (int i = 0; i < historicalOp.getDiscreteAccessCount(); i++) {
169                         AttributedOpEntry attributedOpEntry = historicalOp.getDiscreteAccessAt(i);
170                         logs.add(
171                                 new AccessLog(
172                                         packageName,
173                                         recordTypes,
174                                         attributedOpEntry.getLastAccessTime(
175                                                 OP_FLAG_SELF
176                                                         | OP_FLAG_TRUSTED_PROXIED
177                                                         | OP_FLAG_TRUSTED_PROXY),
178                                         AccessLog.OperationType.OPERATION_TYPE_READ));
179                     }
180                 }
181             }
182         }
183         return logs;
184     }
185 
isWatch()186     private boolean isWatch() {
187         return mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH);
188     }
189 
190     /** Returns all record types that are associated with a granular AppOp. */
getRecordsWithSystemAppOps()191     public Set<Integer> getRecordsWithSystemAppOps() {
192         return mGranularHealthAppOps.stream()
193                 .map(AppOpLogsHelper::opNameToRecordType)
194                 .collect(Collectors.toSet());
195     }
196 
197     /** Returns the RecordTypeIdentifier for the given AppOp name. */
198     @VisibleForTesting
getGranularRecordType(String opName)199     int getGranularRecordType(String opName) {
200         if (!mGranularHealthAppOps.contains(opName)) {
201             return RecordTypeIdentifier.RECORD_TYPE_UNKNOWN;
202         }
203 
204         return opNameToRecordType(opName);
205     }
206 
207     @VisibleForTesting
opNameToRecordType(String opName)208     static int opNameToRecordType(String opName) {
209         switch (opName) {
210             case AppOpsManager.OPSTR_READ_HEART_RATE:
211                 return RecordTypeIdentifier.RECORD_TYPE_HEART_RATE;
212             case AppOpsManager.OPSTR_READ_OXYGEN_SATURATION:
213                 return RecordTypeIdentifier.RECORD_TYPE_OXYGEN_SATURATION;
214             case AppOpsManager.OPSTR_READ_SKIN_TEMPERATURE:
215                 return RecordTypeIdentifier.RECORD_TYPE_SKIN_TEMPERATURE;
216             default:
217                 return RecordTypeIdentifier.RECORD_TYPE_UNKNOWN;
218         }
219     }
220 }
221