• 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.util.SimpleBroadcastReceiver.getPackageFilter;
22 
23 import android.app.RemoteAction;
24 import android.content.ContentProviderClient;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.LauncherApps;
30 import android.database.ContentObserver;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.DeadObjectException;
34 import android.os.Handler;
35 import android.os.Process;
36 import android.os.UserHandle;
37 import android.text.TextUtils;
38 import android.util.ArrayMap;
39 import android.util.Log;
40 import android.view.View;
41 
42 import androidx.annotation.MainThread;
43 import androidx.annotation.Nullable;
44 import androidx.annotation.WorkerThread;
45 
46 import com.android.launcher3.R;
47 import com.android.launcher3.dagger.ApplicationContext;
48 import com.android.launcher3.dagger.LauncherAppSingleton;
49 import com.android.launcher3.model.data.ItemInfo;
50 import com.android.launcher3.popup.RemoteActionShortcut;
51 import com.android.launcher3.popup.SystemShortcut;
52 import com.android.launcher3.util.DaggerSingletonObject;
53 import com.android.launcher3.util.DaggerSingletonTracker;
54 import com.android.launcher3.util.Executors;
55 import com.android.launcher3.util.Preconditions;
56 import com.android.launcher3.util.SafeCloseable;
57 import com.android.launcher3.util.SimpleBroadcastReceiver;
58 import com.android.launcher3.views.ActivityContext;
59 import com.android.quickstep.dagger.QuickstepBaseAppComponent;
60 
61 import java.util.Arrays;
62 import java.util.HashMap;
63 import java.util.Map;
64 
65 import javax.inject.Inject;
66 
67 /**
68  * Data model for digital wellbeing status of apps.
69  */
70 @LauncherAppSingleton
71 public final class WellbeingModel implements SafeCloseable {
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     // Welbeing contract
77     private static final String PATH_ACTIONS = "actions";
78     private static final String METHOD_GET_ACTIONS = "get_actions";
79     private static final String EXTRA_ACTIONS = "actions";
80     private static final String EXTRA_ACTION = "action";
81     private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
82     private static final String EXTRA_PACKAGES = "packages";
83     private static final String EXTRA_SUCCESS = "success";
84 
85     public static final DaggerSingletonObject<WellbeingModel> INSTANCE =
86             new DaggerSingletonObject<>(QuickstepBaseAppComponent::getWellbeingModel);
87 
88     private final Context mContext;
89     private final String mWellbeingProviderPkg;
90 
91     private final Handler mWorkerHandler;
92     private final ContentObserver mContentObserver;
93     private final SimpleBroadcastReceiver mWellbeingAppChangeReceiver;
94     private final SimpleBroadcastReceiver mAppAddRemoveReceiver;
95 
96     private final Object mModelLock = new Object();
97     // Maps the action Id to the corresponding RemoteAction
98     private final Map<String, RemoteAction> mActionIdMap = new ArrayMap<>();
99     private final Map<String, String> mPackageToActionId = new HashMap<>();
100 
101     private boolean mIsInTest;
102 
103     @Inject
WellbeingModel(@pplicationContext final Context context, DaggerSingletonTracker tracker)104     WellbeingModel(@ApplicationContext final Context context,
105             DaggerSingletonTracker tracker) {
106         mContext = context;
107         mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
108         mWorkerHandler = new Handler(TextUtils.isEmpty(mWellbeingProviderPkg)
109                 ? Executors.UI_HELPER_EXECUTOR.getLooper()
110                 : Executors.getPackageExecutor(mWellbeingProviderPkg).getLooper());
111         mWellbeingAppChangeReceiver =
112                 new SimpleBroadcastReceiver(context, mWorkerHandler, t -> restartObserver());
113         mAppAddRemoveReceiver =
114                 new SimpleBroadcastReceiver(context, mWorkerHandler, this::onAppPackageChanged);
115 
116 
117         mContentObserver = new ContentObserver(mWorkerHandler) {
118             @Override
119             public void onChange(boolean selfChange, Uri uri) {
120                 updateAllPackages();
121             }
122         };
123         mWorkerHandler.post(this::initializeInBackground);
124         tracker.addCloseable(this);
125     }
126 
127     @WorkerThread
initializeInBackground()128     private void initializeInBackground() {
129         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
130             mContext.registerReceiver(
131                     mWellbeingAppChangeReceiver,
132                     getPackageFilter(mWellbeingProviderPkg,
133                             Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
134                             Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
135                             Intent.ACTION_PACKAGE_RESTARTED),
136                     null, mWorkerHandler);
137 
138             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
139             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
140             filter.addDataScheme("package");
141             mContext.registerReceiver(mAppAddRemoveReceiver, filter, null, mWorkerHandler);
142 
143             restartObserver();
144         }
145     }
146 
147     @Override
close()148     public void close() {
149         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
150             mWorkerHandler.post(() -> {
151                 mWellbeingAppChangeReceiver.unregisterReceiverSafely();
152                 mAppAddRemoveReceiver.unregisterReceiverSafely();
153                 mContext.getContentResolver().unregisterContentObserver(mContentObserver);
154             });
155         }
156     }
157 
setInTest(boolean inTest)158     public void setInTest(boolean inTest) {
159         mIsInTest = inTest;
160     }
161 
162     @WorkerThread
restartObserver()163     private void restartObserver() {
164         final ContentResolver resolver = mContext.getContentResolver();
165         resolver.unregisterContentObserver(mContentObserver);
166         Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
167         try {
168             resolver.registerContentObserver(
169                     actionsUri, true /* notifyForDescendants */, mContentObserver);
170         } catch (Exception e) {
171             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
172             if (mIsInTest) throw new RuntimeException(e);
173         }
174         updateAllPackages();
175     }
176 
177     @MainThread
getShortcutForApp(String packageName, int userId, Context context, ItemInfo info, View originalView)178     private SystemShortcut getShortcutForApp(String packageName, int userId,
179             Context context, ItemInfo info, View originalView) {
180         Preconditions.assertUIThread();
181         // Work profile apps are not recognized by digital wellbeing.
182         if (userId != UserHandle.myUserId()) {
183             if (DEBUG || mIsInTest) {
184                 Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user");
185             }
186             return null;
187         }
188 
189         synchronized (mModelLock) {
190             String actionId = mPackageToActionId.get(packageName);
191             final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null;
192             if (action == null) {
193                 if (DEBUG || mIsInTest) {
194                     Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action");
195                 }
196                 return null;
197             }
198             if (DEBUG || mIsInTest) {
199                 Log.d(TAG,
200                         "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle()
201                                 + "'");
202             }
203             return new RemoteActionShortcut(action, context, info, originalView);
204         }
205     }
206 
apiBuilder()207     private Uri.Builder apiBuilder() {
208         return new Uri.Builder()
209                 .scheme(SCHEME_CONTENT)
210                 .authority(mWellbeingProviderPkg + ".api");
211     }
212 
213     @WorkerThread
updateActions(String[] packageNames)214     private boolean updateActions(String[] packageNames) {
215         if (packageNames.length == 0) {
216             return true;
217         }
218         if (DEBUG || mIsInTest) {
219             Log.d(TAG, "retrieveActions() called with: packageNames = [" + String.join(", ",
220                     packageNames) + "]");
221         }
222         Preconditions.assertNonUiThread();
223 
224         Uri contentUri = apiBuilder().build();
225         final Bundle remoteActionBundle;
226         try (ContentProviderClient client = mContext.getContentResolver()
227                 .acquireUnstableContentProviderClient(contentUri)) {
228             if (client == null) {
229                 if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): null provider");
230                 return false;
231             }
232 
233             // Prepare wellbeing call parameters.
234             final Bundle params = new Bundle();
235             params.putStringArray(EXTRA_PACKAGES, packageNames);
236             params.putInt(EXTRA_MAX_NUM_ACTIONS_SHOWN, 1);
237             // Perform wellbeing call .
238             remoteActionBundle = client.call(METHOD_GET_ACTIONS, null, params);
239             if (!remoteActionBundle.getBoolean(EXTRA_SUCCESS, true)) return false;
240 
241             synchronized (mModelLock) {
242                 // Remove the entries for requested packages, and then update the fist with what we
243                 // got from service
244                 Arrays.stream(packageNames).forEach(mPackageToActionId::remove);
245 
246                 // The result consists of sub-bundles, each one is per a remote action. Each
247                 // sub-bundle has a RemoteAction and a list of packages to which the action applies.
248                 for (String actionId :
249                         remoteActionBundle.getStringArray(EXTRA_ACTIONS)) {
250                     final Bundle actionBundle = remoteActionBundle.getBundle(actionId);
251                     mActionIdMap.put(actionId,
252                             actionBundle.getParcelable(EXTRA_ACTION));
253 
254                     final String[] packagesForAction =
255                             actionBundle.getStringArray(EXTRA_PACKAGES);
256                     if (DEBUG || mIsInTest) {
257                         Log.d(TAG, "....actionId: " + actionId + ", packages: " + String.join(", ",
258                                 packagesForAction));
259                     }
260                     for (String packageName : packagesForAction) {
261                         mPackageToActionId.put(packageName, actionId);
262                     }
263                 }
264             }
265         } catch (DeadObjectException e) {
266             Log.i(TAG, "retrieveActions(): DeadObjectException");
267             return false;
268         } catch (Exception e) {
269             Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e);
270             if (mIsInTest) throw new RuntimeException(e);
271             return true;
272         }
273         if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): finished");
274         return true;
275     }
276 
277     @WorkerThread
updateActionsWithRetry(int retryCount, @Nullable String packageName)278     private void updateActionsWithRetry(int retryCount, @Nullable String packageName) {
279         if (DEBUG || mIsInTest) {
280             Log.i(TAG,
281                     "updateActionsWithRetry(); retryCount: " + retryCount + ", package: "
282                             + packageName);
283         }
284         String[] packageNames = TextUtils.isEmpty(packageName)
285                 ? mContext.getSystemService(LauncherApps.class)
286                 .getActivityList(null, Process.myUserHandle()).stream()
287                 .map(li -> li.getApplicationInfo().packageName).distinct()
288                 .toArray(String[]::new)
289                 : new String[]{packageName};
290 
291         mWorkerHandler.removeCallbacksAndMessages(packageName);
292         if (updateActions(packageNames)) {
293             return;
294         }
295         if (retryCount >= RETRY_TIMES_MS.length) {
296             // To many retries, skip
297             return;
298         }
299         mWorkerHandler.postDelayed(
300                 () -> {
301                     if (DEBUG || mIsInTest) Log.i(TAG, "Retrying; attempt " + (retryCount + 1));
302                     updateActionsWithRetry(retryCount + 1, packageName);
303                 },
304                 packageName, RETRY_TIMES_MS[retryCount]);
305     }
306 
307     @WorkerThread
updateAllPackages()308     private void updateAllPackages() {
309         if (DEBUG || mIsInTest) Log.i(TAG, "updateAllPackages");
310         updateActionsWithRetry(0, null);
311     }
312 
313     @WorkerThread
onAppPackageChanged(Intent intent)314     private void onAppPackageChanged(Intent intent) {
315         if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
316         Preconditions.assertNonUiThread();
317 
318         final String packageName = intent.getData().getSchemeSpecificPart();
319         if (packageName == null || packageName.length() == 0) {
320             // they sent us a bad intent
321             return;
322         }
323         final String action = intent.getAction();
324         if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
325             mWorkerHandler.removeCallbacksAndMessages(packageName);
326             synchronized (mModelLock) {
327                 mPackageToActionId.remove(packageName);
328             }
329         } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
330             updateActionsWithRetry(0, packageName);
331         }
332     }
333 
334     /**
335      * Shortcut factory for generating wellbeing action
336      */
337     public static final SystemShortcut.Factory<ActivityContext> SHORTCUT_FACTORY =
338             (context, info, originalView) ->
339                     (info.getTargetComponent() == null) ? null
340                             : INSTANCE.get(originalView.getContext()).getShortcutForApp(
341                                     info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
342                                     ActivityContext.lookupContext(originalView.getContext()),
343                                     info, originalView);
344 }
345