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