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