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;
22 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY;
23 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NOT_NULL;
24 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
25 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorIntegerList;
27 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
28 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorStringList;
29 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
30 
31 import android.annotation.NonNull;
32 import android.content.ContentValues;
33 import android.database.Cursor;
34 import android.health.connect.changelog.ChangeLogTokenRequest;
35 import android.util.Pair;
36 
37 import com.android.server.healthconnect.storage.TransactionManager;
38 import com.android.server.healthconnect.storage.request.CreateTableRequest;
39 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
40 import com.android.server.healthconnect.storage.request.ReadTableRequest;
41 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
42 import com.android.server.healthconnect.storage.utils.StorageUtils;
43 import com.android.server.healthconnect.storage.utils.WhereClauses;
44 
45 import java.time.Instant;
46 import java.time.temporal.ChronoUnit;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * A class to interact with the DB table that stores the information about the change log requests
52  * i.e. {@code TABLE_NAME}
53  *
54  * <p>This class returns the row_id of the change_log_request_table as a token, that can later be
55  * used to recreate the request.
56  *
57  * @hide
58  */
59 public final class ChangeLogsRequestHelper extends DatabaseHelper {
60     static final int DEFAULT_CHANGE_LOG_TIME_PERIOD_IN_DAYS = 32;
61     private static final String TABLE_NAME = "change_log_request_table";
62     private static final String PACKAGES_TO_FILTERS_COLUMN_NAME = "packages_to_filter";
63     private static final String RECORD_TYPES_COLUMN_NAME = "record_types";
64     private static final String PACKAGE_NAME_COLUMN_NAME = "package_name";
65     private static final String ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME = "row_id_change_logs_table";
66     private static final String TIME_COLUMN_NAME = "time";
67 
68     @Override
getMainTableName()69     protected String getMainTableName() {
70         return TABLE_NAME;
71     }
72 
73     @NonNull
getCreateTableRequest()74     public static CreateTableRequest getCreateTableRequest() {
75         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
76     }
77 
78     @NonNull
getToken( @onNull String packageName, @NonNull ChangeLogTokenRequest request)79     public static String getToken(
80             @NonNull String packageName, @NonNull ChangeLogTokenRequest request) {
81         ContentValues contentValues = new ContentValues();
82 
83         /**
84          * Store package names here as a package name and not as {@link AppInfoHelper.AppInfo#mId}
85          * as ID might not be available right now but might become available when the actual request
86          * for this token comes
87          */
88         contentValues.put(
89                 PACKAGES_TO_FILTERS_COLUMN_NAME,
90                 String.join(DELIMITER, request.getPackageNamesToFilter()));
91         contentValues.put(
92                 RECORD_TYPES_COLUMN_NAME,
93                 StorageUtils.flattenIntArray(request.getRecordTypesArray()));
94         contentValues.put(PACKAGE_NAME_COLUMN_NAME, packageName);
95         contentValues.put(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, ChangeLogsHelper.getLatestRowId());
96         contentValues.put(TIME_COLUMN_NAME, Instant.now().toEpochMilli());
97 
98         return String.valueOf(
99                 TransactionManager.getInitialisedInstance()
100                         .insert(new UpsertTableRequest(TABLE_NAME, contentValues)));
101     }
102 
getDeleteRequestForAutoDelete()103     public static DeleteTableRequest getDeleteRequestForAutoDelete() {
104         return new DeleteTableRequest(TABLE_NAME)
105                 .setTimeFilter(
106                         TIME_COLUMN_NAME,
107                         Instant.EPOCH.toEpochMilli(),
108                         Instant.now()
109                                 .minus(DEFAULT_CHANGE_LOG_TIME_PERIOD_IN_DAYS, ChronoUnit.DAYS)
110                                 .toEpochMilli());
111     }
112 
113     @NonNull
getColumnInfo()114     private static List<Pair<String, String>> getColumnInfo() {
115         List<Pair<String, String>> columnInfo = new ArrayList<>();
116         columnInfo.add(new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY));
117         columnInfo.add(new Pair<>(PACKAGES_TO_FILTERS_COLUMN_NAME, TEXT_NOT_NULL));
118         columnInfo.add(new Pair<>(PACKAGE_NAME_COLUMN_NAME, TEXT_NOT_NULL));
119         columnInfo.add(new Pair<>(RECORD_TYPES_COLUMN_NAME, TEXT_NULL));
120         columnInfo.add(new Pair<>(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, INTEGER));
121         columnInfo.add(new Pair<>(TIME_COLUMN_NAME, INTEGER));
122 
123         return columnInfo;
124     }
125 
126     @NonNull
getRequest(@onNull String packageName, @NonNull String token)127     public static TokenRequest getRequest(@NonNull String packageName, @NonNull String token) {
128         ReadTableRequest readTableRequest =
129                 new ReadTableRequest(TABLE_NAME)
130                         .setWhereClause(
131                                 new WhereClauses(AND)
132                                         .addWhereEqualsClause(PRIMARY_COLUMN_NAME, token)
133                                         .addWhereEqualsClause(
134                                                 PACKAGE_NAME_COLUMN_NAME, packageName));
135         TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
136         try (Cursor cursor = transactionManager.read(readTableRequest)) {
137             if (!cursor.moveToFirst()) {
138                 throw new IllegalArgumentException("Invalid token");
139             }
140 
141             return new TokenRequest(
142                     getCursorStringList(cursor, PACKAGES_TO_FILTERS_COLUMN_NAME, DELIMITER),
143                     getCursorIntegerList(cursor, RECORD_TYPES_COLUMN_NAME, DELIMITER),
144                     getCursorString(cursor, PACKAGE_NAME_COLUMN_NAME),
145                     getCursorInt(cursor, ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME));
146         }
147     }
148 
149     @NonNull
getNextPageToken(TokenRequest changeLogTokenRequest, long nextRowId)150     public static String getNextPageToken(TokenRequest changeLogTokenRequest, long nextRowId) {
151         ContentValues contentValues = new ContentValues();
152         contentValues.put(
153                 PACKAGES_TO_FILTERS_COLUMN_NAME,
154                 String.join(DELIMITER, changeLogTokenRequest.getPackageNamesToFilter()));
155         contentValues.put(
156                 RECORD_TYPES_COLUMN_NAME,
157                 StorageUtils.flattenIntList(changeLogTokenRequest.getRecordTypes()));
158         contentValues.put(
159                 PACKAGE_NAME_COLUMN_NAME, changeLogTokenRequest.getRequestingPackageName());
160         contentValues.put(ROW_ID_CHANGE_LOGS_TABLE_COLUMN_NAME, nextRowId);
161 
162         return String.valueOf(
163                 TransactionManager.getInitialisedInstance()
164                         .insert(new UpsertTableRequest(TABLE_NAME, contentValues)));
165     }
166 
167     /** A class to represent the request corresponding to a token */
168     public static final class TokenRequest {
169         private final List<String> mPackageNamesToFilter;
170         private final List<Integer> mRecordTypes;
171         private final String mRequestingPackageName;
172         private final long mRowIdChangeLogs;
173 
174         /**
175          * @param requestingPackageName contributing package name
176          * @param packageNamesToFilter package names to filter
177          * @param recordTypes records to filter
178          * @param rowIdChangeLogs row id of change log table after which the logs are to be fetched
179          */
TokenRequest( @onNull List<String> packageNamesToFilter, @NonNull List<Integer> recordTypes, @NonNull String requestingPackageName, long rowIdChangeLogs)180         public TokenRequest(
181                 @NonNull List<String> packageNamesToFilter,
182                 @NonNull List<Integer> recordTypes,
183                 @NonNull String requestingPackageName,
184                 long rowIdChangeLogs) {
185             mPackageNamesToFilter = packageNamesToFilter;
186             mRecordTypes = recordTypes;
187             mRequestingPackageName = requestingPackageName;
188             mRowIdChangeLogs = rowIdChangeLogs;
189         }
190 
getRowIdChangeLogs()191         public long getRowIdChangeLogs() {
192             return mRowIdChangeLogs;
193         }
194 
195         @NonNull
getRequestingPackageName()196         public String getRequestingPackageName() {
197             return mRequestingPackageName;
198         }
199 
200         @NonNull
getPackageNamesToFilter()201         public List<String> getPackageNamesToFilter() {
202             return mPackageNamesToFilter;
203         }
204 
205         @NonNull
getRecordTypes()206         public List<Integer> getRecordTypes() {
207             return mRecordTypes;
208         }
209     }
210 }
211