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