1 /* 2 * Copyright (C) 2021 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.launcher3.model; 18 19 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 20 21 import static java.lang.annotation.RetentionPolicy.SOURCE; 22 23 import android.content.Context; 24 import android.content.pm.LauncherActivityInfo; 25 import android.content.pm.LauncherApps; 26 import android.os.Process; 27 import android.util.ArrayMap; 28 import android.util.Log; 29 30 import androidx.annotation.IntDef; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.WorkerThread; 33 import androidx.room.Room; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.launcher3.model.AppShareabilityDatabase.ShareabilityDao; 37 import com.android.launcher3.util.MainThreadInitializedObject; 38 39 import java.lang.annotation.Retention; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.function.Consumer; 44 45 /** 46 * This class maintains the shareability status of installed apps. 47 * Each app's status is retrieved from the Play Store's API. Statuses are cached in order 48 * to limit extraneous calls to that API (which can be time-consuming). 49 */ 50 public class AppShareabilityManager { 51 @Retention(SOURCE) 52 @IntDef({ 53 ShareabilityStatus.UNKNOWN, 54 ShareabilityStatus.NOT_SHAREABLE, 55 ShareabilityStatus.SHAREABLE 56 }) 57 public @interface ShareabilityStatus { 58 int UNKNOWN = 0; 59 int NOT_SHAREABLE = 1; 60 int SHAREABLE = 2; 61 } 62 63 private static final String TAG = "AppShareabilityManager"; 64 private static final String DB_NAME = "shareabilityDatabase"; 65 public static MainThreadInitializedObject<AppShareabilityManager> INSTANCE = 66 new MainThreadInitializedObject<>(AppShareabilityManager::new); 67 68 private final Context mContext; 69 // Local map to store the data in memory for quick access 70 private final Map<String, Integer> mDataMap; 71 // Database to persist the data across reboots 72 private AppShareabilityDatabase mDatabase; 73 // Data Access Object for the database 74 private ShareabilityDao mDao; 75 // Class to perform shareability checks 76 private AppShareabilityChecker mShareChecker; 77 AppShareabilityManager(Context context)78 private AppShareabilityManager(Context context) { 79 mContext = context; 80 mDataMap = new ArrayMap<>(); 81 mDatabase = Room.databaseBuilder(mContext, AppShareabilityDatabase.class, DB_NAME).build(); 82 mDao = mDatabase.shareabilityDao(); 83 MODEL_EXECUTOR.post(this::readFromDB); 84 } 85 86 /** 87 * Set the shareability checker. The checker determines whether given apps are shareable. 88 * This must be set before the manager can update its data. 89 * @param checker Implementation of AppShareabilityChecker to perform the checks 90 */ setShareabilityChecker(AppShareabilityChecker checker)91 public void setShareabilityChecker(AppShareabilityChecker checker) { 92 mShareChecker = checker; 93 } 94 95 /** 96 * Retrieve the ShareabilityStatus of an app from the local map 97 * This does not interact with the saved database 98 * @param packageName The app's package name 99 * @return The status as a ShareabilityStatus integer 100 */ getStatus(String packageName)101 public synchronized @ShareabilityStatus int getStatus(String packageName) { 102 @ShareabilityStatus int status = ShareabilityStatus.UNKNOWN; 103 if (mDataMap.containsKey(packageName)) { 104 status = mDataMap.get(packageName); 105 } 106 return status; 107 } 108 109 /** 110 * Set the status of a given app. This updates the local map as well as the saved database. 111 */ setStatus(String packageName, @ShareabilityStatus int status)112 public synchronized void setStatus(String packageName, @ShareabilityStatus int status) { 113 mDataMap.put(packageName, status); 114 115 // Write to the database on a separate thread 116 MODEL_EXECUTOR.post(() -> 117 mDao.insertAppStatus(new AppShareabilityStatus(packageName, status))); 118 } 119 120 /** 121 * Set the statuses of given apps. This updates the local map as well as the saved database. 122 */ setStatuses(List<AppShareabilityStatus> statuses)123 public synchronized void setStatuses(List<AppShareabilityStatus> statuses) { 124 for (int i = 0, size = statuses.size(); i < size; i++) { 125 AppShareabilityStatus entry = statuses.get(i); 126 mDataMap.put(entry.packageName, entry.status); 127 } 128 129 // Write to the database on a separate thread 130 MODEL_EXECUTOR.post(() -> 131 mDao.insertAppStatuses(statuses.toArray(new AppShareabilityStatus[0]))); 132 } 133 134 /** 135 * Request a status update for a specific app 136 * @param packageName The app's package name 137 * @param callback Optional callback to be called when the update is complete. The received 138 * Boolean denotes whether the update was successful. 139 */ requestAppStatusUpdate(String packageName, @Nullable Consumer<Boolean> callback)140 public void requestAppStatusUpdate(String packageName, @Nullable Consumer<Boolean> callback) { 141 MODEL_EXECUTOR.post(() -> updateCache(packageName, callback)); 142 } 143 144 /** 145 * Request a status update for all apps 146 */ requestFullUpdate()147 public void requestFullUpdate() { 148 MODEL_EXECUTOR.post(this::updateCache); 149 } 150 151 /** 152 * Update the cached shareability data for all installed apps 153 */ 154 @WorkerThread updateCache()155 private void updateCache() { 156 updateCache(/* packageName */ null, /* callback */ null); 157 } 158 159 /** 160 * Update the cached shareability data 161 * @param packageName A specific package to update. If null, all installed apps will be updated. 162 * @param callback Optional callback to be called when the update is complete. The received 163 * Boolean denotes whether the update was successful. 164 */ 165 @WorkerThread updateCache(@ullable String packageName, @Nullable Consumer<Boolean> callback)166 private void updateCache(@Nullable String packageName, @Nullable Consumer<Boolean> callback) { 167 if (mShareChecker == null) { 168 Log.e(TAG, "AppShareabilityChecker not set"); 169 return; 170 } 171 172 List<String> packageNames = new ArrayList<>(); 173 if (packageName != null) { 174 packageNames.add(packageName); 175 } else { 176 LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class); 177 List<LauncherActivityInfo> installedApps = 178 launcherApps.getActivityList(/* packageName */ null, Process.myUserHandle()); 179 for (int i = 0, size = installedApps.size(); i < size; i++) { 180 packageNames.add(installedApps.get(i).getApplicationInfo().packageName); 181 } 182 } 183 184 mShareChecker.checkApps(packageNames, this, callback); 185 } 186 187 @WorkerThread readFromDB()188 private synchronized void readFromDB() { 189 mDataMap.clear(); 190 List<AppShareabilityStatus> entries = mDao.getAllEntries(); 191 for (int i = 0, size = entries.size(); i < size; i++) { 192 AppShareabilityStatus entry = entries.get(i); 193 mDataMap.put(entry.packageName, entry.status); 194 } 195 } 196 197 /** 198 * Provides a testable instance of this class 199 * This instance allows database queries on the main thread 200 * @hide */ 201 @VisibleForTesting getTestInstance(Context context)202 public static AppShareabilityManager getTestInstance(Context context) { 203 AppShareabilityManager manager = new AppShareabilityManager(context); 204 manager.mDatabase.close(); 205 manager.mDatabase = Room.inMemoryDatabaseBuilder(context, AppShareabilityDatabase.class) 206 .allowMainThreadQueries() 207 .build(); 208 manager.mDao = manager.mDatabase.shareabilityDao(); 209 return manager; 210 } 211 } 212