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