/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.model; import static android.content.ContentResolver.SCHEME_CONTENT; import static com.android.launcher3.Utilities.newContentObserver; import android.annotation.TargetApi; import android.app.RemoteAction; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.LauncherApps; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.DeadObjectException; import android.os.Handler; import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherProvider; import com.android.launcher3.LauncherSettings; import com.android.launcher3.R; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.popup.RemoteActionShortcut; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.util.BgObjectWithLooper; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.PackageManagerHelper; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SimpleBroadcastReceiver; import java.util.Arrays; import java.util.HashMap; import java.util.Map; /** * Data model for digital wellbeing status of apps. */ @TargetApi(Build.VERSION_CODES.Q) public final class WellbeingModel extends BgObjectWithLooper { private static final String TAG = "WellbeingModel"; private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000}; private static final boolean DEBUG = false; private static final int UNKNOWN_MINIMAL_DEVICE_STATE = 0; private static final int IN_MINIMAL_DEVICE = 2; // Welbeing contract private static final String PATH_ACTIONS = "actions"; private static final String PATH_MINIMAL_DEVICE = "minimal_device"; private static final String METHOD_GET_MINIMAL_DEVICE_CONFIG = "get_minimal_device_config"; private static final String METHOD_GET_ACTIONS = "get_actions"; private static final String EXTRA_ACTIONS = "actions"; private static final String EXTRA_ACTION = "action"; private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown"; private static final String EXTRA_PACKAGES = "packages"; private static final String EXTRA_SUCCESS = "success"; private static final String EXTRA_MINIMAL_DEVICE_STATE = "minimal_device_state"; private static final String DB_NAME_MINIMAL_DEVICE = "minimal.db"; public static final MainThreadInitializedObject INSTANCE = new MainThreadInitializedObject<>(WellbeingModel::new); private final Context mContext; private final String mWellbeingProviderPkg; private Handler mWorkerHandler; private ContentObserver mContentObserver; private final Object mModelLock = new Object(); // Maps the action Id to the corresponding RemoteAction private final Map mActionIdMap = new ArrayMap<>(); private final Map mPackageToActionId = new HashMap<>(); private boolean mIsInTest; private WellbeingModel(final Context context) { mContext = context; mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg); initializeInBackground("WellbeingHandler"); } @Override protected void onInitialized(Looper looper) { mWorkerHandler = new Handler(looper); mContentObserver = newContentObserver(mWorkerHandler, this::onWellbeingUriChanged); if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { mContext.registerReceiver( new SimpleBroadcastReceiver(t -> restartObserver()), PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg, Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED, Intent.ACTION_PACKAGE_RESTARTED), null, mWorkerHandler); IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addDataScheme("package"); mContext.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged), filter, null, mWorkerHandler); restartObserver(); } } @WorkerThread private void onWellbeingUriChanged(Uri uri) { Preconditions.assertNonUiThread(); if (DEBUG || mIsInTest) { Log.d(TAG, "ContentObserver.onChange() called with: uri = [" + uri + "]"); } if (uri.getPath().contains(PATH_ACTIONS)) { // Wellbeing reports that app actions have changed. updateAllPackages(); } else if (uri.getPath().contains(PATH_MINIMAL_DEVICE)) { // Wellbeing reports that minimal device state or config is changed. if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) { return; } // Temporary bug fix for b/169771796. Wellbeing provides the layout configuration when // minimal device is enabled. We always want to reload the configuration from Wellbeing // since the layout configuration might have changed. mContext.deleteDatabase(DB_NAME_MINIMAL_DEVICE); final Bundle extras = new Bundle(); String dbFile; if (isInMinimalDeviceMode()) { dbFile = DB_NAME_MINIMAL_DEVICE; extras.putString(LauncherProvider.KEY_LAYOUT_PROVIDER_AUTHORITY, mWellbeingProviderPkg + ".api"); } else { dbFile = InvariantDeviceProfile.INSTANCE.get(mContext).dbFile; } LauncherSettings.Settings.call(mContext.getContentResolver(), LauncherSettings.Settings.METHOD_SWITCH_DATABASE, dbFile, extras); } } public void setInTest(boolean inTest) { mIsInTest = inTest; } @WorkerThread private void restartObserver() { final ContentResolver resolver = mContext.getContentResolver(); resolver.unregisterContentObserver(mContentObserver); Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build(); Uri minimalDeviceUri = apiBuilder().path(PATH_MINIMAL_DEVICE).build(); try { resolver.registerContentObserver( actionsUri, true /* notifyForDescendants */, mContentObserver); resolver.registerContentObserver( minimalDeviceUri, true /* notifyForDescendants */, mContentObserver); } catch (Exception e) { Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e); if (mIsInTest) throw new RuntimeException(e); } updateAllPackages(); } @MainThread private SystemShortcut getShortcutForApp(String packageName, int userId, BaseDraggingActivity activity, ItemInfo info) { Preconditions.assertUIThread(); // Work profile apps are not recognized by digital wellbeing. if (userId != UserHandle.myUserId()) { if (DEBUG || mIsInTest) { Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user"); } return null; } synchronized (mModelLock) { String actionId = mPackageToActionId.get(packageName); final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null; if (action == null) { if (DEBUG || mIsInTest) { Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action"); } return null; } if (DEBUG || mIsInTest) { Log.d(TAG, "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle() + "'"); } return new RemoteActionShortcut(action, activity, info); } } private Uri.Builder apiBuilder() { return new Uri.Builder() .scheme(SCHEME_CONTENT) .authority(mWellbeingProviderPkg + ".api"); } @WorkerThread private boolean isInMinimalDeviceMode() { if (!FeatureFlags.ENABLE_MINIMAL_DEVICE.get()) { return false; } if (DEBUG || mIsInTest) { Log.d(TAG, "isInMinimalDeviceMode() called"); } Preconditions.assertNonUiThread(); final Uri contentUri = apiBuilder().build(); try (ContentProviderClient client = mContext.getContentResolver() .acquireUnstableContentProviderClient(contentUri)) { final Bundle remoteBundle = client == null ? null : client.call( METHOD_GET_MINIMAL_DEVICE_CONFIG, null /* args */, null /* extras */); return remoteBundle != null && remoteBundle.getInt(EXTRA_MINIMAL_DEVICE_STATE, UNKNOWN_MINIMAL_DEVICE_STATE) == IN_MINIMAL_DEVICE; } catch (Exception e) { Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e); if (mIsInTest) throw new RuntimeException(e); } if (DEBUG || mIsInTest) Log.i(TAG, "isInMinimalDeviceMode(): finished"); return false; } @WorkerThread private boolean updateActions(String[] packageNames) { if (packageNames.length == 0) { return true; } if (DEBUG || mIsInTest) { Log.d(TAG, "retrieveActions() called with: packageNames = [" + String.join(", ", packageNames) + "]"); } Preconditions.assertNonUiThread(); Uri contentUri = apiBuilder().build(); final Bundle remoteActionBundle; try (ContentProviderClient client = mContext.getContentResolver() .acquireUnstableContentProviderClient(contentUri)) { if (client == null) { if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): null provider"); return false; } // Prepare wellbeing call parameters. final Bundle params = new Bundle(); params.putStringArray(EXTRA_PACKAGES, packageNames); params.putInt(EXTRA_MAX_NUM_ACTIONS_SHOWN, 1); // Perform wellbeing call . remoteActionBundle = client.call(METHOD_GET_ACTIONS, null, params); if (!remoteActionBundle.getBoolean(EXTRA_SUCCESS, true)) return false; synchronized (mModelLock) { // Remove the entries for requested packages, and then update the fist with what we // got from service Arrays.stream(packageNames).forEach(mPackageToActionId::remove); // The result consists of sub-bundles, each one is per a remote action. Each // sub-bundle has a RemoteAction and a list of packages to which the action applies. for (String actionId : remoteActionBundle.getStringArray(EXTRA_ACTIONS)) { final Bundle actionBundle = remoteActionBundle.getBundle(actionId); mActionIdMap.put(actionId, actionBundle.getParcelable(EXTRA_ACTION)); final String[] packagesForAction = actionBundle.getStringArray(EXTRA_PACKAGES); if (DEBUG || mIsInTest) { Log.d(TAG, "....actionId: " + actionId + ", packages: " + String.join(", ", packagesForAction)); } for (String packageName : packagesForAction) { mPackageToActionId.put(packageName, actionId); } } } } catch (DeadObjectException e) { Log.i(TAG, "retrieveActions(): DeadObjectException"); return false; } catch (Exception e) { Log.e(TAG, "Failed to retrieve data from " + contentUri + ": " + e); if (mIsInTest) throw new RuntimeException(e); return true; } if (DEBUG || mIsInTest) Log.i(TAG, "retrieveActions(): finished"); return true; } @WorkerThread private void updateActionsWithRetry(int retryCount, @Nullable String packageName) { if (DEBUG || mIsInTest) { Log.i(TAG, "updateActionsWithRetry(); retryCount: " + retryCount + ", package: " + packageName); } String[] packageNames = TextUtils.isEmpty(packageName) ? mContext.getSystemService(LauncherApps.class) .getActivityList(null, Process.myUserHandle()).stream() .map(li -> li.getApplicationInfo().packageName).distinct() .toArray(String[]::new) : new String[]{packageName}; mWorkerHandler.removeCallbacksAndMessages(packageName); if (updateActions(packageNames)) { return; } if (retryCount >= RETRY_TIMES_MS.length) { // To many retries, skip return; } mWorkerHandler.postDelayed( () -> { if (DEBUG || mIsInTest) Log.i(TAG, "Retrying; attempt " + (retryCount + 1)); updateActionsWithRetry(retryCount + 1, packageName); }, packageName, RETRY_TIMES_MS[retryCount]); } @WorkerThread private void updateAllPackages() { if (DEBUG || mIsInTest) Log.i(TAG, "updateAllPackages"); updateActionsWithRetry(0, null); } @WorkerThread private void onAppPackageChanged(Intent intent) { if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]"); Preconditions.assertNonUiThread(); final String packageName = intent.getData().getSchemeSpecificPart(); if (packageName == null || packageName.length() == 0) { // they sent us a bad intent return; } final String action = intent.getAction(); if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { mWorkerHandler.removeCallbacksAndMessages(packageName); synchronized (mModelLock) { mPackageToActionId.remove(packageName); } } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { updateActionsWithRetry(0, packageName); } } /** * Shortcut factory for generating wellbeing action */ public static final SystemShortcut.Factory SHORTCUT_FACTORY = (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity) .getShortcutForApp( info.getTargetComponent().getPackageName(), info.user.getIdentifier(), activity, info); }