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