• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.health.connect.Constants.DEFAULT_LONG;
20 import static android.health.connect.accesslog.AccessLog.OperationType.OPERATION_TYPE_DELETE;
21 import static android.health.connect.accesslog.AccessLog.OperationType.OPERATION_TYPE_READ;
22 import static android.health.connect.accesslog.AccessLog.OperationType.OPERATION_TYPE_UPSERT;
23 
24 import static com.android.internal.util.Preconditions.checkArgument;
25 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.BOOLEAN_FALSE_VALUE;
27 import static com.android.server.healthconnect.storage.utils.StorageUtils.BOOLEAN_TRUE_VALUE;
28 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER;
29 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER;
30 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL;
31 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
32 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL;
33 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
34 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
35 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorIntegerList;
36 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
37 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
38 
39 import android.content.ContentValues;
40 import android.content.pm.PackageManager;
41 import android.database.Cursor;
42 import android.database.sqlite.SQLiteDatabase;
43 import android.health.connect.accesslog.AccessLog;
44 import android.health.connect.accesslog.AccessLog.OperationType;
45 import android.health.connect.datatypes.MedicalResource.MedicalResourceType;
46 import android.health.connect.datatypes.RecordTypeIdentifier;
47 import android.os.UserHandle;
48 import android.util.Pair;
49 import android.util.Slog;
50 
51 import com.android.healthfitness.flags.AconfigFlagHelper;
52 import com.android.internal.annotations.VisibleForTesting;
53 import com.android.server.healthconnect.storage.TransactionManager;
54 import com.android.server.healthconnect.storage.request.AlterTableRequest;
55 import com.android.server.healthconnect.storage.request.CreateTableRequest;
56 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
57 import com.android.server.healthconnect.storage.request.ReadTableRequest;
58 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
59 import com.android.server.healthconnect.storage.utils.OrderByClause;
60 import com.android.server.healthconnect.storage.utils.WhereClauses;
61 
62 import java.time.Instant;
63 import java.time.temporal.ChronoUnit;
64 import java.util.ArrayList;
65 import java.util.HashSet;
66 import java.util.List;
67 import java.util.Set;
68 import java.util.stream.Collectors;
69 
70 /**
71  * A helper class to fetch and store the access logs.
72  *
73  * @hide
74  */
75 public final class AccessLogsHelper extends DatabaseHelper {
76     public static final String TABLE_NAME = "access_logs_table";
77     private static final String RECORD_TYPE_COLUMN_NAME = "record_type";
78     private static final String APP_ID_COLUMN_NAME = "app_id";
79     private static final String ACCESS_TIME_COLUMN_NAME = "access_time";
80     private static final String OPERATION_TYPE_COLUMN_NAME = "operation_type";
81     private static final String TAG = "AccessLogHelper";
82 
83     @VisibleForTesting
84     static final String MEDICAL_RESOURCE_TYPE_COLUMN_NAME = "medical_resource_type";
85 
86     @VisibleForTesting
87     static final String MEDICAL_DATA_SOURCE_ACCESSED_COLUMN_NAME = "medical_data_source_accessed";
88 
89     private static final int NUM_COLS = 5;
90     private static final int DEFAULT_ACCESS_LOG_TIME_PERIOD_IN_DAYS = 7;
91 
92     private final TransactionManager mTransactionManager;
93     private final AppInfoHelper mAppInfoHelper;
94     private final AppOpLogsHelper mAppOpLogsHelper;
95 
AccessLogsHelper( TransactionManager transactionManager, AppInfoHelper appInfoHelper, AppOpLogsHelper appOpLogsHelper, DatabaseHelpers databaseHelpers)96     public AccessLogsHelper(
97             TransactionManager transactionManager,
98             AppInfoHelper appInfoHelper,
99             AppOpLogsHelper appOpLogsHelper,
100             DatabaseHelpers databaseHelpers) {
101         super(databaseHelpers);
102         mTransactionManager = transactionManager;
103         mAppInfoHelper = appInfoHelper;
104         mAppOpLogsHelper = appOpLogsHelper;
105     }
106 
getCreateTableRequest()107     public static CreateTableRequest getCreateTableRequest() {
108         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
109     }
110 
111     /**
112      * @return AccessLog list
113      */
queryAccessLogs(UserHandle callingUserHandle)114     public List<AccessLog> queryAccessLogs(UserHandle callingUserHandle) {
115         final ReadTableRequest readTableRequest = new ReadTableRequest(TABLE_NAME);
116 
117         List<AccessLog> accessLogsList = new ArrayList<>();
118         try (Cursor cursor = mTransactionManager.read(readTableRequest)) {
119             while (cursor.moveToNext()) {
120                 String packageName;
121                 try {
122                     packageName =
123                             mAppInfoHelper.getPackageName(
124                                     getCursorLong(cursor, APP_ID_COLUMN_NAME));
125                 } catch (PackageManager.NameNotFoundException e) {
126                     Slog.e(TAG, "Package name not found while query access logs", e);
127                     continue;
128                 }
129                 @RecordTypeIdentifier.RecordType
130                 List<Integer> recordTypes =
131                         getCursorIntegerList(cursor, RECORD_TYPE_COLUMN_NAME, DELIMITER);
132                 long accessTime = getCursorLong(cursor, ACCESS_TIME_COLUMN_NAME);
133                 @OperationType.OperationTypes
134                 int operationType = getCursorInt(cursor, OPERATION_TYPE_COLUMN_NAME);
135                 if (!recordTypes.isEmpty()) {
136                     accessLogsList.add(
137                             new AccessLog(packageName, recordTypes, accessTime, operationType));
138                 }
139                 if (AconfigFlagHelper.isPersonalHealthRecordEnabled()) {
140                     @MedicalResourceType
141                     List<Integer> medicalResourceTypes =
142                             getCursorIntegerList(
143                                     cursor, MEDICAL_RESOURCE_TYPE_COLUMN_NAME, DELIMITER);
144                     boolean isMedicalDataSource =
145                             getCursorInt(cursor, MEDICAL_DATA_SOURCE_ACCESSED_COLUMN_NAME)
146                                     == BOOLEAN_TRUE_VALUE;
147                     if (!medicalResourceTypes.isEmpty() || isMedicalDataSource) {
148                         accessLogsList.add(
149                                 new AccessLog(
150                                         packageName,
151                                         accessTime,
152                                         operationType,
153                                         new HashSet<>(medicalResourceTypes),
154                                         isMedicalDataSource));
155                     }
156                 }
157             }
158         }
159 
160         accessLogsList.addAll(mAppOpLogsHelper.getAccessLogsFromAppOps(callingUserHandle));
161         return accessLogsList;
162     }
163 
164     /**
165      * Returns the timestamp of the latest access log and {@link Long#MIN_VALUE} if there is no
166      * access log.
167      */
168     // TODO: b/364643016 - Should this include the AppOps data as well?
getLatestUpsertOrReadOperationAccessLogTimeStamp()169     public long getLatestUpsertOrReadOperationAccessLogTimeStamp() {
170         final ReadTableRequest readTableRequest =
171                 new ReadTableRequest(TABLE_NAME)
172                         .setWhereClause(
173                                 new WhereClauses(AND)
174                                         .addWhereInIntsClause(
175                                                 OPERATION_TYPE_COLUMN_NAME,
176                                                 List.of(
177                                                         OPERATION_TYPE_READ,
178                                                         OPERATION_TYPE_UPSERT)))
179                         .setOrderBy(
180                                 new OrderByClause()
181                                         .addOrderByClause(ACCESS_TIME_COLUMN_NAME, false))
182                         .setLimit(1);
183 
184         long mostRecentAccessTime = Long.MIN_VALUE;
185         try (Cursor cursor = mTransactionManager.read(readTableRequest)) {
186             while (cursor.moveToNext()) {
187                 long accessTime = getCursorLong(cursor, ACCESS_TIME_COLUMN_NAME);
188                 mostRecentAccessTime = Math.max(mostRecentAccessTime, accessTime);
189             }
190         }
191         return mostRecentAccessTime;
192     }
193 
194     /**
195      * Adds an entry into the {@link AccessLogsHelper#TABLE_NAME} for every insert or read operation
196      * request for record datatypes.
197      *
198      * @deprecated Use {@link #recordReadAccessLog} instead
199      */
200     @Deprecated
addAccessLog( String packageName, @RecordTypeIdentifier.RecordType List<Integer> recordTypeList, @OperationType.OperationTypes int operationType)201     public void addAccessLog(
202             String packageName,
203             @RecordTypeIdentifier.RecordType List<Integer> recordTypeList,
204             @OperationType.OperationTypes int operationType) {
205         long appInfoId = mAppInfoHelper.getAppInfoId(packageName);
206         if (appInfoId == DEFAULT_LONG) {
207             // TODO(b/371210803): Add server side log for this error
208             Slog.w(TAG, "invalid package name " + packageName + " used for access log");
209             return;
210         }
211         UpsertTableRequest request =
212                 getUpsertTableRequest(appInfoId, recordTypeList, operationType);
213         mTransactionManager.insert(request);
214     }
215 
216     /**
217      * Adds an entry into the {@link AccessLogsHelper#TABLE_NAME} for every upsert/read/delete
218      * operation request for medicalResourceTypes.
219      */
addAccessLog( SQLiteDatabase db, String packageName, @MedicalResourceType Set<Integer> medicalResourceTypes, @OperationType.OperationTypes int operationType, boolean accessedMedicalDataSource)220     public void addAccessLog(
221             SQLiteDatabase db,
222             String packageName,
223             @MedicalResourceType Set<Integer> medicalResourceTypes,
224             @OperationType.OperationTypes int operationType,
225             boolean accessedMedicalDataSource) {
226         long appInfoId = mAppInfoHelper.getAppInfoId(packageName);
227         if (appInfoId == DEFAULT_LONG) {
228             // TODO(b/371210803): Add server side log for this error
229             Slog.w(TAG, "invalid package name " + packageName + " used for access log");
230             return;
231         }
232         UpsertTableRequest request =
233                 getUpsertTableRequestForPhr(
234                         appInfoId, medicalResourceTypes, operationType, accessedMedicalDataSource);
235         mTransactionManager.insert(db, request);
236     }
237 
getUpsertTableRequestForPhr( long appInfoId, Set<Integer> medicalResourceTypes, @OperationType.OperationTypes int operationType, boolean isMedicalDataSource)238     private static UpsertTableRequest getUpsertTableRequestForPhr(
239             long appInfoId,
240             Set<Integer> medicalResourceTypes,
241             @OperationType.OperationTypes int operationType,
242             boolean isMedicalDataSource) {
243         checkArgument(appInfoId != DEFAULT_LONG, "unknown app id");
244         // We need to populate RECORD_TYPE_COLUMN_NAME with an empty list, as the column is set
245         // to NOT_NULL.
246         ContentValues contentValues =
247                 populateCommonColumns(appInfoId, /* recordTypeList= */ List.of(), operationType);
248         contentValues.put(
249                 MEDICAL_RESOURCE_TYPE_COLUMN_NAME, concatDataTypeIds(medicalResourceTypes));
250         contentValues.put(
251                 MEDICAL_DATA_SOURCE_ACCESSED_COLUMN_NAME,
252                 isMedicalDataSource ? BOOLEAN_TRUE_VALUE : BOOLEAN_FALSE_VALUE);
253         return new UpsertTableRequest(TABLE_NAME, contentValues);
254     }
255 
getUpsertTableRequest( long appInfoId, List<Integer> recordTypeList, @OperationType.OperationTypes int operationType)256     private static UpsertTableRequest getUpsertTableRequest(
257             long appInfoId,
258             List<Integer> recordTypeList,
259             @OperationType.OperationTypes int operationType) {
260         checkArgument(appInfoId != DEFAULT_LONG, "unknown app id");
261         ContentValues contentValues =
262                 populateCommonColumns(appInfoId, recordTypeList, operationType);
263         return new UpsertTableRequest(TABLE_NAME, contentValues);
264     }
265 
266     /** Adds an entry of read type into the {@link AccessLogsHelper#TABLE_NAME} */
recordReadAccessLog( SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds)267     public void recordReadAccessLog(
268             SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds) {
269         recordAccessLog(db, packageName, recordTypeIds, OPERATION_TYPE_READ);
270     }
271 
272     /** Adds an entry of upsert type into the {@link AccessLogsHelper#TABLE_NAME} */
recordUpsertAccessLog( SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds)273     public void recordUpsertAccessLog(
274             SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds) {
275         recordAccessLog(db, packageName, recordTypeIds, OPERATION_TYPE_UPSERT);
276     }
277 
278     /** Adds an entry of delete type into the {@link AccessLogsHelper#TABLE_NAME} */
recordDeleteAccessLog( SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds)279     public void recordDeleteAccessLog(
280             SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds) {
281         recordAccessLog(db, packageName, recordTypeIds, OPERATION_TYPE_DELETE);
282     }
283 
recordAccessLog( SQLiteDatabase db, String packageName, Set<Integer> recordTypeIds, @OperationType.OperationTypes int operationType)284     private void recordAccessLog(
285             SQLiteDatabase db,
286             String packageName,
287             Set<Integer> recordTypeIds,
288             @OperationType.OperationTypes int operationType) {
289         long appInfoId = mAppInfoHelper.getAppInfoId(packageName);
290         if (appInfoId == DEFAULT_LONG) {
291             // TODO(b/371210803): Add server side log for this error
292             Slog.w(TAG, "invalid package name " + packageName + " used for access log");
293             return;
294         }
295 
296         Set<Integer> filteredRecordTypeIds = new HashSet<>(recordTypeIds);
297         // Remove records for system AppOps so that we don't record them twice
298         // (they are already recorded via historical AppOps).
299         if (operationType == AccessLog.OperationType.OPERATION_TYPE_READ) {
300             Set<Integer> recordsWithSystemAppOps = mAppOpLogsHelper.getRecordsWithSystemAppOps();
301             filteredRecordTypeIds.removeAll(recordsWithSystemAppOps);
302         }
303         if (filteredRecordTypeIds.isEmpty()) {
304             return;
305         }
306 
307         ContentValues contentValues =
308                 populateCommonColumns(
309                         appInfoId, filteredRecordTypeIds.stream().toList(), operationType);
310         UpsertTableRequest request = new UpsertTableRequest(TABLE_NAME, contentValues);
311         mTransactionManager.insert(db, request);
312     }
313 
314     @VisibleForTesting
populateCommonColumns( long appInfoId, List<Integer> recordTypeList, @OperationType.OperationTypes int operationType)315     static ContentValues populateCommonColumns(
316             long appInfoId,
317             List<Integer> recordTypeList,
318             @OperationType.OperationTypes int operationType) {
319         checkArgument(appInfoId != DEFAULT_LONG, "unknown app id");
320 
321         ContentValues contentValues = new ContentValues();
322         contentValues.put(APP_ID_COLUMN_NAME, appInfoId);
323         contentValues.put(ACCESS_TIME_COLUMN_NAME, Instant.now().toEpochMilli());
324         contentValues.put(OPERATION_TYPE_COLUMN_NAME, operationType);
325         contentValues.put(
326                 RECORD_TYPE_COLUMN_NAME, concatDataTypeIds(new HashSet<>(recordTypeList)));
327         return contentValues;
328     }
329 
concatDataTypeIds(Set<Integer> dataTypes)330     private static String concatDataTypeIds(Set<Integer> dataTypes) {
331         return dataTypes.stream().map(String::valueOf).collect(Collectors.joining(","));
332     }
333 
334     /**
335      * Returns an instance of {@link DeleteTableRequest} to delete entries in access logs table
336      * older than a week.
337      */
getDeleteRequestForAutoDelete()338     public static DeleteTableRequest getDeleteRequestForAutoDelete() {
339         return new DeleteTableRequest(TABLE_NAME)
340                 .setTimeFilter(
341                         ACCESS_TIME_COLUMN_NAME,
342                         Instant.EPOCH.toEpochMilli(),
343                         Instant.now()
344                                 .minus(DEFAULT_ACCESS_LOG_TIME_PERIOD_IN_DAYS, ChronoUnit.DAYS)
345                                 .toEpochMilli());
346     }
347 
348     /**
349      * Creates an {@link AlterTableRequest} for adding PHR specific columns, {@link
350      * AccessLogsHelper#MEDICAL_RESOURCE_TYPE_COLUMN_NAME} and {@link
351      * AccessLogsHelper#MEDICAL_DATA_SOURCE_ACCESSED_COLUMN_NAME} to the access_logs_table.
352      */
getAlterTableRequestForPhrAccessLogs()353     public static AlterTableRequest getAlterTableRequestForPhrAccessLogs() {
354         return new AlterTableRequest(TABLE_NAME, getPhrAccessLogsColumnInfo());
355     }
356 
getColumnInfo()357     private static List<Pair<String, String>> getColumnInfo() {
358         List<Pair<String, String>> columnInfo = new ArrayList<>(NUM_COLS);
359         columnInfo.add(new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT));
360         columnInfo.add(new Pair<>(APP_ID_COLUMN_NAME, INTEGER_NOT_NULL));
361         columnInfo.add(new Pair<>(RECORD_TYPE_COLUMN_NAME, TEXT_NOT_NULL));
362         columnInfo.add(new Pair<>(ACCESS_TIME_COLUMN_NAME, INTEGER_NOT_NULL));
363         columnInfo.add(new Pair<>(OPERATION_TYPE_COLUMN_NAME, INTEGER_NOT_NULL));
364         return columnInfo;
365     }
366 
367     /** Gets the columns to add for an {@link AlterTableRequest} for adding PHR specific columns, */
getPhrAccessLogsColumnInfo()368     public static List<Pair<String, String>> getPhrAccessLogsColumnInfo() {
369         return List.of(
370                 // This is list of comma separated integers that represent
371                 // the medicalResourceTypes accessed.
372                 Pair.create(MEDICAL_RESOURCE_TYPE_COLUMN_NAME, TEXT_NULL),
373                 // This represents a boolean, which tells us whether
374                 // the MedicalDataSource data is accessed.
375                 Pair.create(MEDICAL_DATA_SOURCE_ACCESSED_COLUMN_NAME, INTEGER));
376     }
377 
378     @Override
getMainTableName()379     protected String getMainTableName() {
380         return TABLE_NAME;
381     }
382 }
383