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 com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME; 20 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER; 21 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL; 22 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT; 23 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL; 24 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt; 25 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorIntegerList; 26 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong; 27 28 import android.annotation.NonNull; 29 import android.content.ContentValues; 30 import android.database.Cursor; 31 import android.health.connect.accesslog.AccessLog; 32 import android.health.connect.accesslog.AccessLog.OperationType; 33 import android.health.connect.datatypes.RecordTypeIdentifier; 34 import android.util.Pair; 35 36 import com.android.server.healthconnect.storage.TransactionManager; 37 import com.android.server.healthconnect.storage.request.CreateTableRequest; 38 import com.android.server.healthconnect.storage.request.DeleteTableRequest; 39 import com.android.server.healthconnect.storage.request.ReadTableRequest; 40 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 41 import com.android.server.healthconnect.storage.utils.OrderByClause; 42 43 import java.time.Instant; 44 import java.time.temporal.ChronoUnit; 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.stream.Collectors; 48 49 /** 50 * A helper class to fetch and store the access logs. 51 * 52 * @hide 53 */ 54 public final class AccessLogsHelper extends DatabaseHelper { 55 public static final String TABLE_NAME = "access_logs_table"; 56 private static final String RECORD_TYPE_COLUMN_NAME = "record_type"; 57 private static final String APP_ID_COLUMN_NAME = "app_id"; 58 private static final String ACCESS_TIME_COLUMN_NAME = "access_time"; 59 private static final String OPERATION_TYPE_COLUMN_NAME = "operation_type"; 60 private static final int NUM_COLS = 5; 61 private static final int DEFAULT_ACCESS_LOG_TIME_PERIOD_IN_DAYS = 7; 62 63 @NonNull getCreateTableRequest()64 public static CreateTableRequest getCreateTableRequest() { 65 return new CreateTableRequest(TABLE_NAME, getColumnInfo()); 66 } 67 68 /** 69 * @return AccessLog list 70 */ queryAccessLogs()71 public static List<AccessLog> queryAccessLogs() { 72 final ReadTableRequest readTableRequest = new ReadTableRequest(TABLE_NAME); 73 74 List<AccessLog> accessLogsList = new ArrayList<>(); 75 final AppInfoHelper appInfoHelper = AppInfoHelper.getInstance(); 76 final TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); 77 try (Cursor cursor = transactionManager.read(readTableRequest)) { 78 while (cursor.moveToNext()) { 79 String packageName = 80 appInfoHelper.getPackageName(getCursorLong(cursor, APP_ID_COLUMN_NAME)); 81 @RecordTypeIdentifier.RecordType 82 List<Integer> recordTypes = 83 getCursorIntegerList(cursor, RECORD_TYPE_COLUMN_NAME, DELIMITER); 84 long accessTime = getCursorLong(cursor, ACCESS_TIME_COLUMN_NAME); 85 @OperationType.OperationTypes 86 int operationType = getCursorInt(cursor, OPERATION_TYPE_COLUMN_NAME); 87 accessLogsList.add( 88 new AccessLog(packageName, recordTypes, accessTime, operationType)); 89 } 90 } 91 92 return accessLogsList; 93 } 94 95 /** 96 * Returns the timestamp of the latest access log and {@link Long.MIN_VALUE} if there is no 97 * access log. 98 */ getLatestAccessLogTimeStamp()99 public static long getLatestAccessLogTimeStamp() { 100 101 final ReadTableRequest readTableRequest = 102 new ReadTableRequest(TABLE_NAME) 103 .setOrderBy( 104 new OrderByClause() 105 .addOrderByClause(ACCESS_TIME_COLUMN_NAME, false)) 106 .setLimit(1); 107 108 long mostRecentAccessTime = Long.MIN_VALUE; 109 final TransactionManager transactionManager = TransactionManager.getInitialisedInstance(); 110 try (Cursor cursor = transactionManager.read(readTableRequest)) { 111 while (cursor.moveToNext()) { 112 long accessTime = getCursorLong(cursor, ACCESS_TIME_COLUMN_NAME); 113 mostRecentAccessTime = Math.max(mostRecentAccessTime, accessTime); 114 } 115 } 116 return mostRecentAccessTime; 117 } 118 119 /** Adds an entry in to the access logs table for every insert or read operation request */ addAccessLog( String packageName, @RecordTypeIdentifier.RecordType List<Integer> recordTypeList, @OperationType.OperationTypes int operationType)120 public static void addAccessLog( 121 String packageName, 122 @RecordTypeIdentifier.RecordType List<Integer> recordTypeList, 123 @OperationType.OperationTypes int operationType) { 124 UpsertTableRequest request = 125 getUpsertTableRequest(packageName, recordTypeList, operationType); 126 TransactionManager.getInitialisedInstance().insert(request); 127 } 128 129 @NonNull getUpsertTableRequest( String packageName, List<Integer> recordTypeList, int operationType)130 public static UpsertTableRequest getUpsertTableRequest( 131 String packageName, List<Integer> recordTypeList, int operationType) { 132 ContentValues contentValues = new ContentValues(); 133 contentValues.put( 134 RECORD_TYPE_COLUMN_NAME, 135 recordTypeList.stream().map(String::valueOf).collect(Collectors.joining(","))); 136 contentValues.put( 137 APP_ID_COLUMN_NAME, AppInfoHelper.getInstance().getAppInfoId(packageName)); 138 contentValues.put(ACCESS_TIME_COLUMN_NAME, Instant.now().toEpochMilli()); 139 contentValues.put(OPERATION_TYPE_COLUMN_NAME, operationType); 140 141 return new UpsertTableRequest(TABLE_NAME, contentValues); 142 } 143 144 /** 145 * Returns an instance of {@link DeleteTableRequest} to delete entries in access logs table 146 * older than a week. 147 */ getDeleteRequestForAutoDelete()148 public static DeleteTableRequest getDeleteRequestForAutoDelete() { 149 return new DeleteTableRequest(TABLE_NAME) 150 .setTimeFilter( 151 ACCESS_TIME_COLUMN_NAME, 152 Instant.EPOCH.toEpochMilli(), 153 Instant.now() 154 .minus(DEFAULT_ACCESS_LOG_TIME_PERIOD_IN_DAYS, ChronoUnit.DAYS) 155 .toEpochMilli()); 156 } 157 158 @NonNull getColumnInfo()159 private static List<Pair<String, String>> getColumnInfo() { 160 List<Pair<String, String>> columnInfo = new ArrayList<>(NUM_COLS); 161 columnInfo.add(new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT)); 162 columnInfo.add(new Pair<>(APP_ID_COLUMN_NAME, INTEGER_NOT_NULL)); 163 columnInfo.add(new Pair<>(RECORD_TYPE_COLUMN_NAME, TEXT_NOT_NULL)); 164 columnInfo.add(new Pair<>(ACCESS_TIME_COLUMN_NAME, INTEGER_NOT_NULL)); 165 columnInfo.add(new Pair<>(OPERATION_TYPE_COLUMN_NAME, INTEGER_NOT_NULL)); 166 167 return columnInfo; 168 } 169 170 @Override getMainTableName()171 protected String getMainTableName() { 172 return TABLE_NAME; 173 } 174 } 175