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