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