• 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.Constants.DELETE;
21 import static android.health.connect.Constants.UPSERT;
22 
23 import static com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsRequestHelper.DEFAULT_CHANGE_LOG_TIME_PERIOD_IN_DAYS;
24 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME;
25 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB_NON_NULL;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER;
27 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
28 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
29 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
30 
31 import android.annotation.NonNull;
32 import android.content.ContentValues;
33 import android.database.Cursor;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.health.connect.accesslog.AccessLog.OperationType;
36 import android.health.connect.changelog.ChangeLogsRequest;
37 import android.health.connect.changelog.ChangeLogsResponse.DeletedLog;
38 import android.health.connect.datatypes.RecordTypeIdentifier;
39 import android.util.ArrayMap;
40 import android.util.Pair;
41 
42 import com.android.server.healthconnect.storage.TransactionManager;
43 import com.android.server.healthconnect.storage.request.CreateTableRequest;
44 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
45 import com.android.server.healthconnect.storage.request.ReadTableRequest;
46 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
47 import com.android.server.healthconnect.storage.utils.StorageUtils;
48 import com.android.server.healthconnect.storage.utils.WhereClauses;
49 
50 import java.time.Instant;
51 import java.time.temporal.ChronoUnit;
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.List;
55 import java.util.Map;
56 import java.util.Objects;
57 import java.util.UUID;
58 import java.util.stream.Collectors;
59 
60 /**
61  * A helper class to fetch and store the change logs.
62  *
63  * @hide
64  */
65 public final class ChangeLogsHelper {
66     public static final String TABLE_NAME = "change_logs_table";
67     private static final String RECORD_TYPE_COLUMN_NAME = "record_type";
68     private static final String APP_ID_COLUMN_NAME = "app_id";
69     private static final String UUIDS_COLUMN_NAME = "uuids";
70     private static final String OPERATION_TYPE_COLUMN_NAME = "operation_type";
71     private static final String TIME_COLUMN_NAME = "time";
72     private static final int NUM_COLS = 5;
73     private static volatile ChangeLogsHelper sChangeLogsHelper;
74 
ChangeLogsHelper()75     private ChangeLogsHelper() {}
76 
getDeleteRequestForAutoDelete()77     public DeleteTableRequest getDeleteRequestForAutoDelete() {
78         return new DeleteTableRequest(TABLE_NAME)
79                 .setTimeFilter(
80                         TIME_COLUMN_NAME,
81                         Instant.EPOCH.toEpochMilli(),
82                         Instant.now()
83                                 .minus(DEFAULT_CHANGE_LOG_TIME_PERIOD_IN_DAYS, ChronoUnit.DAYS)
84                                 .toEpochMilli());
85     }
86 
87     @NonNull
getCreateTableRequest()88     public CreateTableRequest getCreateTableRequest() {
89         return new CreateTableRequest(TABLE_NAME, getColumnInfo())
90                 .createIndexOn(RECORD_TYPE_COLUMN_NAME)
91                 .createIndexOn(APP_ID_COLUMN_NAME);
92     }
93 
94     // Called on DB update.
onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db)95     public void onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db) {}
96 
97     /** Returns change logs post the time when {@code changeLogTokenRequest} was generated */
getChangeLogs( ChangeLogsRequestHelper.TokenRequest changeLogTokenRequest, ChangeLogsRequest changeLogsRequest)98     public ChangeLogsResponse getChangeLogs(
99             ChangeLogsRequestHelper.TokenRequest changeLogTokenRequest,
100             ChangeLogsRequest changeLogsRequest) {
101         long token = changeLogTokenRequest.getRowIdChangeLogs();
102         WhereClauses whereClause =
103                 new WhereClauses()
104                         .addWhereGreaterThanClause(PRIMARY_COLUMN_NAME, String.valueOf(token));
105         if (!changeLogTokenRequest.getRecordTypes().isEmpty()) {
106             whereClause.addWhereInIntsClause(
107                     RECORD_TYPE_COLUMN_NAME, changeLogTokenRequest.getRecordTypes());
108         }
109 
110         if (!changeLogTokenRequest.getPackageNamesToFilter().isEmpty()) {
111             whereClause.addWhereInLongsClause(
112                     APP_ID_COLUMN_NAME,
113                     AppInfoHelper.getInstance()
114                             .getAppInfoIds(changeLogTokenRequest.getPackageNamesToFilter()));
115         }
116 
117         // In setLimit(pagesize) method size will be set to pageSize + 1,so that if number of
118         // records returned is more than pageSize we know there are more records available to return
119         // for the next read.
120         int pageSize = changeLogsRequest.getPageSize();
121         final ReadTableRequest readTableRequest =
122                 new ReadTableRequest(TABLE_NAME).setWhereClause(whereClause).setLimit(pageSize);
123 
124         Map<Integer, ChangeLogs> operationToChangeLogMap = new ArrayMap<>();
125         TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
126         long nextChangesToken = DEFAULT_LONG;
127         boolean hasMoreRecords = false;
128         try (Cursor cursor = transactionManager.read(readTableRequest)) {
129             int count = 0;
130             while (cursor.moveToNext()) {
131                 if (count >= pageSize) {
132                     hasMoreRecords = true;
133                     break;
134                 }
135                 count += addChangeLogs(cursor, operationToChangeLogMap);
136                 nextChangesToken = getCursorInt(cursor, PRIMARY_COLUMN_NAME);
137             }
138         }
139 
140         String nextToken =
141                 nextChangesToken != DEFAULT_LONG
142                         ? ChangeLogsRequestHelper.getNextPageToken(
143                                 changeLogTokenRequest, nextChangesToken)
144                         : String.valueOf(changeLogsRequest.getToken());
145 
146         return new ChangeLogsResponse(operationToChangeLogMap, nextToken, hasMoreRecords);
147     }
148 
getLatestRowId()149     public long getLatestRowId() {
150         return TransactionManager.getInitialisedInstance().getLastRowIdFor(TABLE_NAME);
151     }
152 
addChangeLogs(Cursor cursor, Map<Integer, ChangeLogs> changeLogs)153     private int addChangeLogs(Cursor cursor, Map<Integer, ChangeLogs> changeLogs) {
154         @RecordTypeIdentifier.RecordType
155         int recordType = getCursorInt(cursor, RECORD_TYPE_COLUMN_NAME);
156         @OperationType.OperationTypes
157         int operationType = getCursorInt(cursor, OPERATION_TYPE_COLUMN_NAME);
158         List<UUID> uuidList = StorageUtils.getCursorUUIDList(cursor, UUIDS_COLUMN_NAME);
159         long appId = getCursorLong(cursor, APP_ID_COLUMN_NAME);
160         changeLogs.putIfAbsent(
161                 operationType,
162                 new ChangeLogs(operationType, getCursorLong(cursor, TIME_COLUMN_NAME)));
163         changeLogs.get(operationType).addUUIDs(recordType, appId, uuidList);
164         return uuidList.size();
165     }
166 
167     @NonNull
getColumnInfo()168     private List<Pair<String, String>> getColumnInfo() {
169         List<Pair<String, String>> columnInfo = new ArrayList<>(NUM_COLS);
170         columnInfo.add(new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT));
171         columnInfo.add(new Pair<>(RECORD_TYPE_COLUMN_NAME, INTEGER));
172         columnInfo.add(new Pair<>(APP_ID_COLUMN_NAME, INTEGER));
173         columnInfo.add(new Pair<>(UUIDS_COLUMN_NAME, BLOB_NON_NULL));
174         columnInfo.add(new Pair<>(OPERATION_TYPE_COLUMN_NAME, INTEGER));
175         columnInfo.add(new Pair<>(TIME_COLUMN_NAME, INTEGER));
176 
177         return columnInfo;
178     }
179 
getInstance()180     public static synchronized ChangeLogsHelper getInstance() {
181         if (sChangeLogsHelper == null) {
182             sChangeLogsHelper = new ChangeLogsHelper();
183         }
184 
185         return sChangeLogsHelper;
186     }
187 
188     @NonNull
getDeletedLogs(Map<Integer, ChangeLogs> operationToChangeLogs)189     public static List<DeletedLog> getDeletedLogs(Map<Integer, ChangeLogs> operationToChangeLogs) {
190         ChangeLogs logs = operationToChangeLogs.get(DELETE);
191 
192         if (!Objects.isNull(logs)) {
193             List<UUID> ids = logs.getUUIds();
194             long timeStamp = logs.getChangeLogTimeStamp();
195             List<DeletedLog> deletedLogs = new ArrayList<>(ids.size());
196             for (UUID id : ids) {
197                 deletedLogs.add(new DeletedLog(id.toString(), timeStamp));
198             }
199 
200             return deletedLogs;
201         }
202         return new ArrayList<>();
203     }
204 
205     @NonNull
getRecordTypeToInsertedUuids( Map<Integer, ChangeLogs> operationToChangeLogs)206     public static Map<Integer, List<UUID>> getRecordTypeToInsertedUuids(
207             Map<Integer, ChangeLogs> operationToChangeLogs) {
208         ChangeLogs logs = operationToChangeLogs.getOrDefault(UPSERT, null);
209 
210         if (!Objects.isNull(logs)) {
211             return logs.getRecordTypeToUUIDMap();
212         }
213 
214         return new ArrayMap<>(0);
215     }
216 
217     public static final class ChangeLogs {
218         private final Map<RecordTypeAndAppIdPair, List<UUID>> mRecordTypeAndAppIdToUUIDMap =
219                 new ArrayMap<>();
220         @OperationType.OperationTypes private final int mOperationType;
221         private final String mPackageName;
222         private final long mChangeLogTimeStamp;
223         /**
224          * Creates a change logs object used to add a new change log for {@code operationType} for
225          * {@code packageName} logged at time {@code timeStamp }
226          *
227          * @param operationType Type of the operation for which change log is added whether insert
228          *     or delete.
229          * @param packageName Package name of the records for which change log is added.
230          * @param timeStamp Time when the change log is added.
231          */
ChangeLogs( @perationType.OperationTypes int operationType, @NonNull String packageName, long timeStamp)232         public ChangeLogs(
233                 @OperationType.OperationTypes int operationType,
234                 @NonNull String packageName,
235                 long timeStamp) {
236             mOperationType = operationType;
237             mPackageName = packageName;
238             mChangeLogTimeStamp = timeStamp;
239         }
240         /**
241          * Creates a change logs object used to add a new change log for {@code operationType}
242          * logged at time {@code timeStamp }
243          *
244          * @param operationType Type of the operation for which change log is added whether insert
245          *     or delete.
246          * @param timeStamp Time when the change log is added.
247          */
ChangeLogs(@perationType.OperationTypes int operationType, long timeStamp)248         public ChangeLogs(@OperationType.OperationTypes int operationType, long timeStamp) {
249             mOperationType = operationType;
250             mChangeLogTimeStamp = timeStamp;
251             mPackageName = null;
252         }
253 
getRecordTypeToUUIDMap()254         public Map<Integer, List<UUID>> getRecordTypeToUUIDMap() {
255             Map<Integer, List<UUID>> recordTypeToUUIDMap = new ArrayMap<>();
256             mRecordTypeAndAppIdToUUIDMap.forEach(
257                     (recordTypeAndAppIdPair, uuids) -> {
258                         recordTypeToUUIDMap.putIfAbsent(
259                                 recordTypeAndAppIdPair.getRecordType(), new ArrayList<>());
260                         Objects.requireNonNull(
261                                         recordTypeToUUIDMap.get(
262                                                 recordTypeAndAppIdPair.getRecordType()))
263                                 .addAll(uuids);
264                     });
265             return recordTypeToUUIDMap;
266         }
267 
getUUIds()268         public List<UUID> getUUIds() {
269             return mRecordTypeAndAppIdToUUIDMap.values().stream()
270                     .flatMap(Collection::stream)
271                     .collect(Collectors.toList());
272         }
273 
getChangeLogTimeStamp()274         public long getChangeLogTimeStamp() {
275             return mChangeLogTimeStamp;
276         }
277 
278         /** Function to add an uuid corresponding to given pair of @recordType and @appId */
addUUID( @ecordTypeIdentifier.RecordType int recordType, @NonNull long appId, @NonNull UUID uuid)279         public void addUUID(
280                 @RecordTypeIdentifier.RecordType int recordType,
281                 @NonNull long appId,
282                 @NonNull UUID uuid) {
283             Objects.requireNonNull(uuid);
284 
285             RecordTypeAndAppIdPair recordTypeAndAppIdPair =
286                     new RecordTypeAndAppIdPair(recordType, appId);
287             mRecordTypeAndAppIdToUUIDMap.putIfAbsent(recordTypeAndAppIdPair, new ArrayList<>());
288             mRecordTypeAndAppIdToUUIDMap.get(recordTypeAndAppIdPair).add(uuid);
289         }
290 
291         /**
292          * @return List of {@link UpsertTableRequest} for change log table as per {@code
293          *     mRecordTypeAndAppIdPairToUUIDMap}
294          */
getUpsertTableRequests()295         public List<UpsertTableRequest> getUpsertTableRequests() {
296             Objects.requireNonNull(mPackageName);
297 
298             List<UpsertTableRequest> requests =
299                     new ArrayList<>(mRecordTypeAndAppIdToUUIDMap.size());
300             mRecordTypeAndAppIdToUUIDMap.forEach(
301                     (recordTypeAndAppIdPair, uuids) -> {
302                         ContentValues contentValues = new ContentValues();
303                         contentValues.put(
304                                 RECORD_TYPE_COLUMN_NAME, recordTypeAndAppIdPair.getRecordType());
305                         contentValues.put(APP_ID_COLUMN_NAME, recordTypeAndAppIdPair.getAppId());
306                         contentValues.put(OPERATION_TYPE_COLUMN_NAME, mOperationType);
307                         contentValues.put(TIME_COLUMN_NAME, mChangeLogTimeStamp);
308                         contentValues.put(
309                                 UUIDS_COLUMN_NAME, StorageUtils.getSingleByteArray(uuids));
310                         requests.add(new UpsertTableRequest(TABLE_NAME, contentValues));
311                     });
312             return requests;
313         }
314 
addUUIDs( @ecordTypeIdentifier.RecordType int recordType, @NonNull long appId, @NonNull List<UUID> uuids)315         public ChangeLogs addUUIDs(
316                 @RecordTypeIdentifier.RecordType int recordType,
317                 @NonNull long appId,
318                 @NonNull List<UUID> uuids) {
319             RecordTypeAndAppIdPair recordTypeAndAppIdPair =
320                     new RecordTypeAndAppIdPair(recordType, appId);
321             mRecordTypeAndAppIdToUUIDMap.putIfAbsent(recordTypeAndAppIdPair, new ArrayList<>());
322             mRecordTypeAndAppIdToUUIDMap.get(recordTypeAndAppIdPair).addAll(uuids);
323             return this;
324         }
325 
clear()326         public void clear() {
327             mRecordTypeAndAppIdToUUIDMap.clear();
328         }
329 
330         /** A helper class to create a pair of recordType and appId */
331         private static final class RecordTypeAndAppIdPair {
332             private final int mRecordType;
333             private final long mAppId;
334 
RecordTypeAndAppIdPair(int recordType, long appId)335             private RecordTypeAndAppIdPair(int recordType, long appId) {
336                 mRecordType = recordType;
337                 mAppId = appId;
338             }
339 
getRecordType()340             public int getRecordType() {
341                 return mRecordType;
342             }
343 
getAppId()344             public long getAppId() {
345                 return mAppId;
346             }
347 
equals(Object obj)348             public boolean equals(Object obj) {
349                 if (this == obj) return true;
350                 if (obj == null || obj.getClass() != this.getClass()) return false;
351                 RecordTypeAndAppIdPair recordTypeAndAppIdPair = (RecordTypeAndAppIdPair) obj;
352                 return (recordTypeAndAppIdPair.mRecordType == this.mRecordType
353                         && recordTypeAndAppIdPair.mAppId == this.mAppId);
354             }
355 
hashCode()356             public int hashCode() {
357                 return Objects.hash(this.mRecordType, this.mAppId);
358             }
359         }
360     }
361 
362     /** A class to represent the token for pagination for the change logs response */
363     public static final class ChangeLogsResponse {
364         private final Map<Integer, ChangeLogsHelper.ChangeLogs> mChangeLogsMap;
365         private final String mNextPageToken;
366         private final boolean mHasMorePages;
367 
ChangeLogsResponse( @onNull Map<Integer, ChangeLogsHelper.ChangeLogs> changeLogsMap, @NonNull String nextPageToken, boolean hasMorePages)368         public ChangeLogsResponse(
369                 @NonNull Map<Integer, ChangeLogsHelper.ChangeLogs> changeLogsMap,
370                 @NonNull String nextPageToken,
371                 boolean hasMorePages) {
372             mChangeLogsMap = changeLogsMap;
373             mNextPageToken = nextPageToken;
374             mHasMorePages = hasMorePages;
375         }
376 
377         /** Returns map of operation type to change logs */
378         @NonNull
getChangeLogsMap()379         public Map<Integer, ChangeLogs> getChangeLogsMap() {
380             return mChangeLogsMap;
381         }
382 
383         /** Returns the next page token for the change logs */
384         @NonNull
getNextPageToken()385         public String getNextPageToken() {
386             return mNextPageToken;
387         }
388 
389         /** Returns true if there are more change logs to be read */
hasMorePages()390         public boolean hasMorePages() {
391             return mHasMorePages;
392         }
393     }
394 }
395