• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 android.content.ContentResolver.SCHEME_CONTENT;
20 
21 import static com.android.launcher3.Utilities.newContentObserver;
22 
23 import android.annotation.TargetApi;
24 import android.app.RemoteAction;
25 import android.content.ContentProviderClient;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.LauncherApps;
31 import android.database.ContentObserver;
32 import android.net.Uri;
33 import android.os.Build;
34 import android.os.Bundle;
35 import android.os.DeadObjectException;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.Process;
39 import android.os.UserHandle;
40 import android.text.TextUtils;
41 import android.util.ArrayMap;
42 import android.util.Log;
43 
44 import androidx.annotation.MainThread;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.WorkerThread;
47 
48 import com.android.launcher3.BaseDraggingActivity;
49 import com.android.launcher3.InvariantDeviceProfile;
50 import com.android.launcher3.LauncherProvider;
51 import com.android.launcher3.LauncherSettings;
52 import com.android.launcher3.R;
53 import com.android.launcher3.config.FeatureFlags;
54 import com.android.launcher3.model.data.ItemInfo;
55 import com.android.launcher3.popup.RemoteActionShortcut;
56 import com.android.launcher3.popup.SystemShortcut;
57 import com.android.launcher3.util.BgObjectWithLooper;
58 import com.android.launcher3.util.MainThreadInitializedObject;
59 import com.android.launcher3.util.PackageManagerHelper;
60 import com.android.launcher3.util.Preconditions;
61 import com.android.launcher3.util.SimpleBroadcastReceiver;
62 
63 import java.util.Arrays;
64 import java.util.HashMap;
65 import java.util.Map;
66 
67 /**
68  * Data model for digital wellbeing status of apps.
69  */
70 @TargetApi(Build.VERSION_CODES.Q)
71 public final class WellbeingModel extends BgObjectWithLooper {
72     private static final String TAG = "WellbeingModel";
73     private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
74     private static final boolean DEBUG = false;
75 
76     private static final int UNKNOWN_MINIMAL_DEVICE_STATE = 0;
77     private static final int IN_MINIMAL_DEVICE = 2;
78 
79     // Welbeing contract
80     private static final String PATH_ACTIONS = "actions";
81     private static final String PATH_MINIMAL_DEVICE = "minimal_device";
82     private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config";
83     private static final String METHOD_GET_ACTIONS = "get_actions";
84     private static final String EXTRA_ACTIONS = "actions";
85     private static final String EXTRA_ACTION = "action";
86     private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
87     private static final String EXTRA_PACKAGES = "packages";
88     private static final String EXTRA_SUCCESS = "success";
89     private static final String EXTRA_MINIMAL_DEVICE_STATE = "minimal_device_state";
90     private static final String DB_NAME_MINIMAL_DEVICE = "minimal.db";
91 
92     public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
93             new MainThreadInitializedObject<>(WellbeingModel::new);
94 
95     private final Context mContext;
96     private final String mWellbeingProviderPkg;
97 
98     private Handler mWorkerHandler;
99     private ContentObserver mContentObserver;
100 
101     private final Object mModelLock = new Object();
102     // Maps the action Id to the corresponding RemoteAction
103     private final Map<String, RemoteAction> mActionIdMap = new ArrayMap<>();
104     private final Map<String, String> mPackageToActionId = new HashMap<>();
105 
106     private boolean mIsInTest;
107 
WellbeingModel(final Context context)108     private WellbeingModel(final Context context) {
109         mContext = context;
110         mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
111         initializeInBackground("WellbeingHandler");
112     }
113 
114     @Override
onInitialized(Looper looper)115     protected void onInitialized(Looper looper) {
116         mWorkerHandler = new Handler(looper);
117         mContentObserver = newContentObserver(mWorkerHandler, this::onWellbeingUriChanged);
118         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
119             mContext.registerReceiver(
120                     new SimpleBroadcastReceiver(t -> restartObserver()),
121                     PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg,
122                             Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
123                             Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
124                             Intent.ACTION_PACKAGE_RESTARTED),
125                     null, mWorkerHandler);
126 
127             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
128             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
129             filter.addDataScheme("package");
130             mContext.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
131                     filter, null, mWorkerHandler);
132 
133             restartObserver();
134         }
135     }
136 
137     @WorkerThread
onWellbeingUriChanged(Uri uri)138     private void onWellbeingUriChanged(Uri uri) {
139         Preconditions.assertNonUiThread();
140         if (DEBUG || mIsInTest) {
141             Log.d(TAG, "ContentObserver.onChange() called with: uri = [" + uri + "]");
142         }
143         if (uri.getPath().contains(PATH_ACTIONS)) {
144             // Wellbeing reports that app actions have changed.
145             updateAllPackages();
146         } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) {
147             // Wellbeing reports that minimal device state or config is changed.
148             if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
149                 return;
150             }
151 
152             // Temporary bug fix for b/169771796. Wellbeing provides the layout configuration when
153             // minimal device is enabled. We always want to reload the configuration from Wellbeing
154             // since the layout configuration might have changed.
155             mContext.deleteDatabase(DB_NAME_MINIMAL_DEVICE);
156 
157             final Bundle extras = new Bundle();
158             String dbFile;
159             if (isInMinimalDeviceMode()) {
160                 dbFile = DB_NAME_MINIMAL_DEVICE;
161                 extras.putString(LauncherProvider.KEY_LAYOUT_PROVIDER_AUTHORITY,
162                         mWellbeingProviderPkg + ".api");
163             } else {
164                 dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
165             }
166             LauncherSettings.Settings.call(mContext.getContentResolver(),
167                     LauncherSettings.Settings.METHOD_SWITCH_DATABASE,
168                     dbFile, extras);
169         }
170     }
171 
setInTest(boolean inTest)172     public void setInTest(boolean inTest) {
173         mIsInTest = inTest;
174     }
175 
176     @WorkerThread
restartObserver()177     private void restartObserver() {
178         final ContentResolver resolver = mContext.getContentResolver();
179         resolver.unregisterContentObserver(mContentObserver);
180         Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
181         Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build();
182         try {
183             resolver.registerContentObserver(
184                     actionsUri, true /* notifyForDescendants */, mContentObserver);
185             resolver.registerContentObserver(
186                     minimalDeviceUri, true /* notifyForDescendants */, mContentObserver);
187         } catch (Exception e) {
188             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
189             if (mIsInTest) throw new RuntimeException(e);
190         }
191         updateAllPackages();
192     }
193 
194     @MainThread
getShortcutForApp(String packageName, int userId, BaseDraggingActivity activity, ItemInfo info)195     private SystemShortcut getShortcutForApp(String packageName, int userId,
196             BaseDraggingActivity activity, ItemInfo info) {
197         Preconditions.assertUIThread();
198         // Work profile apps are not recognized by digital wellbeing.
199         if (userId != UserHandle.myUserId()) {
200             if (DEBUG || mIsInTest) {
201                 Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user");
202             }
203             return null;
204         }
205 
206         synchronized (mModelLock) {
207             String actionId = mPackageToActionId.get(packageName);
208             final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null;
209             if (action == null) {
210                 if (DEBUG || mIsInTest) {
211                     Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action");
212                 }
213                 return null;
214             }
215             if (DEBUG || mIsInTest) {
216                 Log.d(TAG,
217                         "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle()
218                                 + "'");
219             }
220             return new RemoteActionShortcut(action, activity, info);
221         }
222     }
223 
apiBuilder()224     private Uri.Builder apiBuilder() {
225         return new Uri.Builder()
226                 .scheme(SCHEME_CONTENT)
227                 .authority(mWellbeingProviderPkg + ".api");
228     }
229 
230     @WorkerThread
isInMinimalDeviceMode()231     private boolean isInMinimalDeviceMode() {
232         if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) {
233             return false;
234         }
235         if (DEBUG || mIsInTest) {
236             Log.d(TAG, "isInMinimalDeviceMode() called");
237         }
238         Preconditions.assertNonUiThread();
239 
240         final Uri contentUri = apiBuilder().build();
241         try (ContentProviderClient client = mContext.getContentResolver()
242                 .acquireUnstableContentProviderClient(contentUri)) {
243             final Bundle remoteBundle = client == null ? null : client.call(
244                     METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */);
245             return remoteBundle != null
246                     && remoteBundle.getInt(EXTRA_MINIMAL_DEVICE_STATE,
247                     UNKNOWN_MINIMAL_DEVICE_STATE) == IN_MINIMAL_DEVICE;
248         } catch (Exception e) {
249             Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
250             if (mIsInTest) throw new RuntimeException(e);
251         }
252         if (DEBUG || mIsInTest) Log.i(TAG, "isInMinimalDeviceMode(): finished");
253         return false;
254     }
255 
256     @WorkerThread
updateActions(String[] packageNames)257     private boolean updateActions(String[] packageNames) {
258         if (packageNames.length == 0) {
259             return true;
260         }
261         if (DEBUG || mIsInTest) {
262             Log.d(TAG, "retrieveActions() called with: packageNames = [" + String.join(", ",
263                     packageNames) + "]");
264         }
265         Preconditions.assertNonUiThread();
266 
267         Uri contentUri = apiBuilder().build();
268         final Bundle remoteActionBundle;
269         try (ContentProviderClient client = mContext.getContentResolver()
270                 .acquireUnstableContentProviderClient(contentUri)) {
271             if (client == null) {
272                 if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): null provider");
273                 return false;
274             }
275 
276             // Prepare wellbeing call parameters.
277             final Bundle params = new Bundle();
278             params.putStringArray(EXTRA_PACKAGES, packageNames);
279             params.putInt(EXTRA_MAX_NUM_ACTIONS_SHOWN, 1);
280             // Perform wellbeing call .
281             remoteActionBundle = client.call(METHOD_GET_ACTIONS, null, params);
282             if (!remoteActionBundle.getBoolean(EXTRA_SUCCESS, true)) return false;
283 
284             synchronized (mModelLock) {
285                 // Remove the entries for requested packages, and then update the fist with what we
286                 // got from service
287                 Arrays.stream(packageNames).forEach(mPackageToActionId::remove);
288 
289                 // The result consists of sub-bundles, each one is per a remote action. Each
290                 // sub-bundle has a RemoteAction and a list of packages to which the action applies.
291                 for (String actionId :
292                         remoteActionBundle.getStringArray(EXTRA_ACTIONS)) {
293                     final Bundle actionBundle = remoteActionBundle.getBundle(actionId);
294                     mActionIdMap.put(actionId,
295                             actionBundle.getParcelable(EXTRA_ACTION));
296 
297                     final String[] packagesForAction =
298                             actionBundle.getStringArray(EXTRA_PACKAGES);
299                     if (DEBUG || mIsInTest) {
300                         Log.d(TAG, "....actionId: " + actionId + ", packages: " + String.join(", ",
301                                 packagesForAction));
302                     }
303                     for (String packageName : packagesForAction) {
304                         mPackageToActionId.put(packageName, actionId);
305                     }
306                 }
307             }
308         } catch (DeadObjectException e) {
309             Log.i(TAG, "retrieveActions(): DeadObjectException");
310             return false;
311         } catch (Exception e) {
312             Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
313             if (mIsInTest) throw new RuntimeException(e);
314             return true;
315         }
316         if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): finished");
317         return true;
318     }
319 
320     @WorkerThread
updateActionsWithRetry(int retryCount, @Nullable String packageName)321     private void updateActionsWithRetry(int retryCount, @Nullable String packageName) {
322         if (DEBUG || mIsInTest) {
323             Log.i(TAG,
324                     "updateActionsWithRetry(); retryCount: " + retryCount + ", package: "
325                             + packageName);
326         }
327         String[] packageNames = TextUtils.isEmpty(packageName)
328                 ? mContext.getSystemService(LauncherApps.class)
329                 .getActivityList(null, Process.myUserHandle()).stream()
330                 .map(li -> li.getApplicationInfo().packageName).distinct()
331                 .toArray(String[]::new)
332                 : new String[]{packageName};
333 
334         mWorkerHandler.removeCallbacksAndMessages(packageName);
335         if (updateActions(packageNames)) {
336             return;
337         }
338         if (retryCount >= RETRY_TIMES_MS.length) {
339             // To many retries, skip
340             return;
341         }
342         mWorkerHandler.postDelayed(
343                 () -> {
344                     if (DEBUG || mIsInTest) Log.i(TAG, "Retrying; attempt " + (retryCount + 1));
345                     updateActionsWithRetry(retryCount + 1, packageName);
346                 },
347                 packageName, RETRY_TIMES_MS[retryCount]);
348     }
349 
350     @WorkerThread
updateAllPackages()351     private void updateAllPackages() {
352         if (DEBUG || mIsInTest) Log.i(TAG, "updateAllPackages");
353         updateActionsWithRetry(0, null);
354     }
355 
356     @WorkerThread
onAppPackageChanged(Intent intent)357     private void onAppPackageChanged(Intent intent) {
358         if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
359         Preconditions.assertNonUiThread();
360 
361         final String packageName = intent.getData().getSchemeSpecificPart();
362         if (packageName == null || packageName.length() == 0) {
363             // they sent us a bad intent
364             return;
365         }
366         final String action = intent.getAction();
367         if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
368             mWorkerHandler.removeCallbacksAndMessages(packageName);
369             synchronized (mModelLock) {
370                 mPackageToActionId.remove(packageName);
371             }
372         } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
373             updateActionsWithRetry(0, packageName);
374         }
375     }
376 
377     /**
378      * Shortcut factory for generating wellbeing action
379      */
380     public static final SystemShortcut.Factory SHORTCUT_FACTORY =
381             (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity)
382                     .getShortcutForApp(
383                             info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
384                             activity, info);
385 }
386