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.Executors.MAIN_EXECUTOR;
22 import static com.android.launcher3.util.Executors.createAndStartNewLooper;
23 
24 import android.annotation.TargetApi;
25 import android.app.RemoteAction;
26 import android.content.ContentProviderClient;
27 import android.content.ContentResolver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.content.pm.LauncherApps;
32 import android.database.ContentObserver;
33 import android.net.Uri;
34 import android.os.Build;
35 import android.os.Bundle;
36 import android.os.DeadObjectException;
37 import android.os.Handler;
38 import android.os.Message;
39 import android.os.Process;
40 import android.os.UserHandle;
41 import android.text.TextUtils;
42 import android.util.ArrayMap;
43 import android.util.Log;
44 
45 import androidx.annotation.MainThread;
46 
47 import com.android.launcher3.BaseDraggingActivity;
48 import com.android.launcher3.R;
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.MainThreadInitializedObject;
53 import com.android.launcher3.util.PackageManagerHelper;
54 import com.android.launcher3.util.Preconditions;
55 import com.android.launcher3.util.SimpleBroadcastReceiver;
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 @TargetApi(Build.VERSION_CODES.Q)
65 public final class WellbeingModel {
66     private static final String TAG = "WellbeingModel";
67     private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
68     private static final boolean DEBUG = false;
69 
70     private static final int MSG_PACKAGE_ADDED = 1;
71     private static final int MSG_PACKAGE_REMOVED = 2;
72     private static final int MSG_FULL_REFRESH = 3;
73 
74     // Welbeing contract
75     private static final String METHOD_GET_ACTIONS = "get_actions";
76     private static final String EXTRA_ACTIONS = "actions";
77     private static final String EXTRA_ACTION = "action";
78     private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown";
79     private static final String EXTRA_PACKAGES = "packages";
80     private static final String EXTRA_SUCCESS = "success";
81 
82     public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
83             new MainThreadInitializedObject<>(WellbeingModel::new);
84 
85     private final Context mContext;
86     private final String mWellbeingProviderPkg;
87     private final Handler mWorkerHandler;
88 
89     private final ContentObserver mContentObserver;
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         mWorkerHandler =
101                 new Handler(createAndStartNewLooper("WellbeingHandler"), this::handleMessage);
102 
103         mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
104         mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
105             @Override
106             public void onChange(boolean selfChange, Uri uri) {
107                 // Wellbeing reports that app actions have changed.
108                 if (DEBUG || mIsInTest) {
109                     Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange
110                             + "], uri = [" + uri + "]");
111                 }
112                 Preconditions.assertUIThread();
113                 updateWellbeingData();
114             }
115         };
116 
117         if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
118             context.registerReceiver(
119                     new SimpleBroadcastReceiver(this::onWellbeingProviderChanged),
120                     PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg,
121                             Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
122                             Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
123                             Intent.ACTION_PACKAGE_RESTARTED));
124 
125             IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
126             filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
127             filter.addDataScheme("package");
128             context.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
129                     filter);
130 
131             restartObserver();
132         }
133     }
134 
setInTest(boolean inTest)135     public void setInTest(boolean inTest) {
136         mIsInTest = inTest;
137     }
138 
onWellbeingProviderChanged(Intent intent)139     protected void onWellbeingProviderChanged(Intent intent) {
140         if (DEBUG || mIsInTest) {
141             Log.d(TAG, "Changes to Wellbeing package: intent = [" + intent + "]");
142         }
143         restartObserver();
144     }
145 
restartObserver()146     private void restartObserver() {
147         final ContentResolver resolver = mContext.getContentResolver();
148         resolver.unregisterContentObserver(mContentObserver);
149         Uri actionsUri = apiBuilder().path("actions").build();
150         try {
151             resolver.registerContentObserver(
152                     actionsUri, true /* notifyForDescendants */, mContentObserver);
153         } catch (Exception e) {
154             Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
155             if (mIsInTest) throw new RuntimeException(e);
156         }
157         updateWellbeingData();
158     }
159 
160     @MainThread
getShortcutForApp(String packageName, int userId, BaseDraggingActivity activity, ItemInfo info)161     private SystemShortcut getShortcutForApp(String packageName, int userId,
162             BaseDraggingActivity activity, ItemInfo info) {
163         Preconditions.assertUIThread();
164         // Work profile apps are not recognized by digital wellbeing.
165         if (userId != UserHandle.myUserId()) {
166             if (DEBUG || mIsInTest) {
167                 Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user");
168             }
169             return null;
170         }
171 
172         synchronized (mModelLock) {
173             String actionId = mPackageToActionId.get(packageName);
174             final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null;
175             if (action == null) {
176                 if (DEBUG || mIsInTest) {
177                     Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action");
178                 }
179                 return null;
180             }
181             if (DEBUG || mIsInTest) {
182                 Log.d(TAG,
183                         "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle()
184                                 + "'");
185             }
186             return new RemoteActionShortcut(action, activity, info);
187         }
188     }
189 
updateWellbeingData()190     private void updateWellbeingData() {
191         mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH);
192     }
193 
apiBuilder()194     private Uri.Builder apiBuilder() {
195         return new Uri.Builder()
196                 .scheme(SCHEME_CONTENT)
197                 .authority(mWellbeingProviderPkg + ".api");
198     }
199 
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 
handleMessage(Message msg)263     private boolean handleMessage(Message msg) {
264         switch (msg.what) {
265             case MSG_PACKAGE_REMOVED: {
266                 String packageName = (String) msg.obj;
267                 mWorkerHandler.removeCallbacksAndMessages(packageName);
268                 synchronized (mModelLock) {
269                     mPackageToActionId.remove(packageName);
270                 }
271                 return true;
272             }
273             case MSG_PACKAGE_ADDED: {
274                 String packageName = (String) msg.obj;
275                 mWorkerHandler.removeCallbacksAndMessages(packageName);
276                 if (!updateActions(packageName)) {
277                     scheduleRefreshRetry(msg);
278                 }
279                 return true;
280             }
281 
282             case MSG_FULL_REFRESH: {
283                 // Remove all existing messages
284                 mWorkerHandler.removeCallbacksAndMessages(null);
285                 final String[] packageNames = mContext.getSystemService(LauncherApps.class)
286                         .getActivityList(null, Process.myUserHandle()).stream()
287                         .map(li -> li.getApplicationInfo().packageName).distinct()
288                         .toArray(String[]::new);
289                 if (!updateActions(packageNames)) {
290                     scheduleRefreshRetry(msg);
291                 }
292                 return true;
293             }
294         }
295         return false;
296     }
297 
scheduleRefreshRetry(Message originalMsg)298     private void scheduleRefreshRetry(Message originalMsg) {
299         int retryCount = originalMsg.arg1;
300         if (retryCount >= RETRY_TIMES_MS.length) {
301             // To many retries, skip
302             return;
303         }
304 
305         Message msg = Message.obtain(originalMsg);
306         msg.arg1 = retryCount + 1;
307         mWorkerHandler.sendMessageDelayed(msg, RETRY_TIMES_MS[retryCount]);
308     }
309 
onAppPackageChanged(Intent intent)310     private void onAppPackageChanged(Intent intent) {
311         if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
312         Preconditions.assertUIThread();
313 
314         final String packageName = intent.getData().getSchemeSpecificPart();
315         if (packageName == null || packageName.length() == 0) {
316             // they sent us a bad intent
317             return;
318         }
319 
320         final String action = intent.getAction();
321         if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
322             Message.obtain(mWorkerHandler, MSG_PACKAGE_REMOVED, packageName).sendToTarget();
323         } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
324             Message.obtain(mWorkerHandler, MSG_PACKAGE_ADDED, packageName).sendToTarget();
325         }
326     }
327 
328     /**
329      * Shortcut factory for generating wellbeing action
330      */
331     public static final SystemShortcut.Factory SHORTCUT_FACTORY =
332             (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity)
333                     .getShortcutForApp(
334                             info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
335                             activity, info);
336 }
337