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