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