1 /* 2 * Copyright (C) 2024 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.ondevicepersonalization.services.data.user; 18 19 import android.annotation.NonNull; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.database.sqlite.SQLiteException; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.odp.module.common.Clock; 28 import com.android.odp.module.common.MonotonicClock; 29 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 30 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper; 31 import com.android.ondevicepersonalization.services.fbs.AppInfo; 32 import com.android.ondevicepersonalization.services.fbs.AppInfoList; 33 34 import com.google.common.primitives.Ints; 35 import com.google.flatbuffers.FlatBufferBuilder; 36 37 import java.nio.ByteBuffer; 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 import java.util.Map; 41 42 public class UserDataDao { 43 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 44 private static final String TAG = UserDataDao.class.getSimpleName(); 45 private static volatile UserDataDao sSingleton; 46 private final OnDevicePersonalizationDbHelper mDbHelper; 47 private final Clock mClock; 48 UserDataDao(@onNull OnDevicePersonalizationDbHelper dbHelper, Clock clock)49 private UserDataDao(@NonNull OnDevicePersonalizationDbHelper dbHelper, Clock clock) { 50 this.mDbHelper = dbHelper; 51 this.mClock = clock; 52 } 53 54 /** Returns an instance of the EventsDao given a context. */ getInstance(@onNull Context context)55 public static UserDataDao getInstance(@NonNull Context context) { 56 if (sSingleton == null) { 57 synchronized (UserDataDao.class) { 58 if (sSingleton == null) { 59 OnDevicePersonalizationDbHelper dbHelper = 60 OnDevicePersonalizationDbHelper.getInstance(context); 61 sSingleton = new UserDataDao(dbHelper, MonotonicClock.getInstance()); 62 } 63 } 64 } 65 return sSingleton; 66 } 67 68 /** Returns an instance of the EventsDao given a context. This is used for testing only. */ 69 @VisibleForTesting getInstanceForTest(@onNull Context context, Clock clock)70 public static UserDataDao getInstanceForTest(@NonNull Context context, Clock clock) { 71 synchronized (UserDataDao.class) { 72 if (sSingleton == null) { 73 OnDevicePersonalizationDbHelper dbHelper = 74 OnDevicePersonalizationDbHelper.getInstanceForTest(context); 75 sSingleton = new UserDataDao(dbHelper, clock); 76 } 77 return sSingleton; 78 } 79 } 80 81 /** Returns an instance of the EventsDao given a context. This is used for testing only. */ 82 @VisibleForTesting getInstanceForTest(@onNull Context context)83 public static UserDataDao getInstanceForTest(@NonNull Context context) { 84 synchronized (UserDataDao.class) { 85 if (sSingleton == null) { 86 OnDevicePersonalizationDbHelper dbHelper = 87 OnDevicePersonalizationDbHelper.getInstanceForTest(context); 88 sSingleton = new UserDataDao(dbHelper, MonotonicClock.getInstance()); 89 } 90 return sSingleton; 91 } 92 } 93 94 /** Inserts or replaces an entry in AppInstall table. */ insertAppInstall(Map<String, Long> appInstallList)95 public boolean insertAppInstall(Map<String, Long> appInstallList) { 96 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 97 if (db == null) { 98 return false; 99 } 100 101 byte[] appInfoList = convertAppInstallFromMapToBytes(appInstallList); 102 ContentValues values = new ContentValues(); 103 values.put(UserDataContract.AppInstall.APP_LIST, appInfoList); 104 values.put(UserDataContract.AppInstall.CREATION_TIME, mClock.currentTimeMillis()); 105 long jobId = 106 db.insertWithOnConflict( 107 UserDataContract.AppInstall.TABLE_NAME, 108 null, 109 values, 110 SQLiteDatabase.CONFLICT_REPLACE); 111 return jobId != -1; 112 } 113 114 /** Gets the app installed map . */ 115 @NonNull getAppInstallMap()116 public Map<String, Long> getAppInstallMap() { 117 Map<String, Long> appInstallMap = new HashMap<>(); 118 119 SQLiteDatabase db = mDbHelper.safeGetReadableDatabase(); 120 if (db == null) { 121 return appInstallMap; 122 } 123 String[] projection = {UserDataContract.AppInstall.APP_LIST}; 124 String orderBy = UserDataContract.AppInstall.CREATION_TIME + " DESC"; 125 try (Cursor cursor = 126 db.query( 127 UserDataContract.AppInstall.TABLE_NAME, 128 projection, 129 null, 130 null, 131 /* groupBy= */ null, 132 /* having= */ null, 133 /* orderBy= */ orderBy)) { 134 if (cursor.moveToNext()) { 135 byte[] blob = 136 cursor.getBlob( 137 cursor.getColumnIndexOrThrow(UserDataContract.AppInstall.APP_LIST)); 138 cursor.close(); 139 return convertAppInstallFromBytesToMap(blob); 140 } 141 } 142 return appInstallMap; 143 } 144 145 /** Deletes all entries in AppInstall table. */ deleteAllAppInstallTable()146 public boolean deleteAllAppInstallTable() { 147 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 148 if (db == null) { 149 return false; 150 } 151 boolean success = false; 152 db.beginTransaction(); 153 try { 154 db.delete(UserDataContract.AppInstall.TABLE_NAME, null, null); 155 success = true; 156 db.setTransactionSuccessful(); 157 } catch (SQLiteException e) { 158 // TODO(b/337481657): add logging for db failure. 159 sLogger.e(e, TAG + ": Failed to perform delete all on AppInstall table."); 160 } finally { 161 db.endTransaction(); 162 } 163 return success; 164 } 165 convertAppInstallFromMapToBytes(Map<String, Long> appInstallMap)166 private byte[] convertAppInstallFromMapToBytes(Map<String, Long> appInstallMap) { 167 FlatBufferBuilder builder = new FlatBufferBuilder(); 168 ArrayList<Integer> entryOffsets = new ArrayList<>(); 169 int offset = 0; 170 for (String packageName : appInstallMap.keySet()) { 171 long updateTime = appInstallMap.get(packageName); 172 offset = builder.createString(packageName); 173 offset = AppInfo.createAppInfo(builder, offset, updateTime); 174 entryOffsets.add(offset); 175 } 176 offset = AppInfoList.createAppInfoListVector(builder, Ints.toArray(entryOffsets)); 177 AppInfoList.startAppInfoList(builder); 178 AppInfoList.addAppInfoList(builder, offset); 179 offset = AppInfoList.endAppInfoList(builder); 180 builder.finish(offset); 181 return builder.sizedByteArray(); 182 } 183 convertAppInstallFromBytesToMap(byte[] blob)184 private Map<String, Long> convertAppInstallFromBytesToMap(byte[] blob) { 185 HashMap<String, Long> appInstallMap = new HashMap<>(); 186 AppInfoList appInfoList = AppInfoList.getRootAsAppInfoList(ByteBuffer.wrap(blob)); 187 for (int i = 0; i < appInfoList.appInfoListLength(); i++) { 188 AppInfo appInfo = appInfoList.appInfoList(i); 189 appInstallMap.put(appInfo.name(), appInfo.updateTime()); 190 } 191 return appInstallMap; 192 } 193 } 194