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