2019-10-23 17:44:27 -07:00
|
|
|
/*
|
|
|
|
|
* 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;
|
|
|
|
|
|
2023-01-10 17:50:32 -08:00
|
|
|
import static com.android.launcher3.util.SimpleBroadcastReceiver.getPackageFilter;
|
|
|
|
|
|
2019-10-23 17:44:27 -07:00
|
|
|
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.Bundle;
|
|
|
|
|
import android.os.DeadObjectException;
|
|
|
|
|
import android.os.Handler;
|
2020-09-29 10:32:32 -07:00
|
|
|
import android.os.Looper;
|
2019-10-23 17:44:27 -07:00
|
|
|
import android.os.Process;
|
|
|
|
|
import android.os.UserHandle;
|
|
|
|
|
import android.text.TextUtils;
|
|
|
|
|
import android.util.ArrayMap;
|
|
|
|
|
import android.util.Log;
|
2022-04-10 00:13:01 -07:00
|
|
|
import android.view.View;
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
import androidx.annotation.MainThread;
|
2020-09-29 10:32:32 -07:00
|
|
|
import androidx.annotation.Nullable;
|
2020-07-20 11:03:39 -07:00
|
|
|
import androidx.annotation.WorkerThread;
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
import com.android.launcher3.R;
|
2020-04-06 15:11:17 -07:00
|
|
|
import com.android.launcher3.model.data.ItemInfo;
|
2019-10-23 17:44:27 -07:00
|
|
|
import com.android.launcher3.popup.RemoteActionShortcut;
|
|
|
|
|
import com.android.launcher3.popup.SystemShortcut;
|
2020-09-29 10:32:32 -07:00
|
|
|
import com.android.launcher3.util.BgObjectWithLooper;
|
2019-12-09 14:55:56 -08:00
|
|
|
import com.android.launcher3.util.MainThreadInitializedObject;
|
2019-10-23 17:44:27 -07:00
|
|
|
import com.android.launcher3.util.Preconditions;
|
|
|
|
|
import com.android.launcher3.util.SimpleBroadcastReceiver;
|
2024-04-11 04:27:23 +00:00
|
|
|
import com.android.launcher3.views.ActivityContext;
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Data model for digital wellbeing status of apps.
|
|
|
|
|
*/
|
2020-09-29 10:32:32 -07:00
|
|
|
public final class WellbeingModel extends BgObjectWithLooper {
|
2019-10-23 17:44:27 -07:00
|
|
|
private static final String TAG = "WellbeingModel";
|
|
|
|
|
private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000};
|
|
|
|
|
private static final boolean DEBUG = false;
|
|
|
|
|
|
|
|
|
|
// Welbeing contract
|
2020-07-20 11:03:39 -07:00
|
|
|
private static final String PATH_ACTIONS = "actions";
|
2019-10-23 17:44:27 -07:00
|
|
|
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";
|
2020-05-29 13:38:44 -07:00
|
|
|
private static final String EXTRA_SUCCESS = "success";
|
2019-10-23 17:44:27 -07:00
|
|
|
|
2019-12-09 14:55:56 -08:00
|
|
|
public static final MainThreadInitializedObject<WellbeingModel> INSTANCE =
|
|
|
|
|
new MainThreadInitializedObject<>(WellbeingModel::new);
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
private final Context mContext;
|
|
|
|
|
private final String mWellbeingProviderPkg;
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
private Handler mWorkerHandler;
|
|
|
|
|
private ContentObserver mContentObserver;
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
private final Object mModelLock = new Object();
|
|
|
|
|
// Maps the action Id to the corresponding RemoteAction
|
|
|
|
|
private final Map<String, RemoteAction> mActionIdMap = new ArrayMap<>();
|
|
|
|
|
private final Map<String, String> mPackageToActionId = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
private boolean mIsInTest;
|
|
|
|
|
|
|
|
|
|
private WellbeingModel(final Context context) {
|
|
|
|
|
mContext = context;
|
|
|
|
|
mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg);
|
2020-09-29 10:32:32 -07:00
|
|
|
initializeInBackground("WellbeingHandler");
|
|
|
|
|
}
|
2019-10-23 17:44:27 -07:00
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@Override
|
|
|
|
|
protected void onInitialized(Looper looper) {
|
|
|
|
|
mWorkerHandler = new Handler(looper);
|
|
|
|
|
mContentObserver = newContentObserver(mWorkerHandler, this::onWellbeingUriChanged);
|
2019-10-23 17:44:27 -07:00
|
|
|
if (!TextUtils.isEmpty(mWellbeingProviderPkg)) {
|
2020-09-29 10:32:32 -07:00
|
|
|
mContext.registerReceiver(
|
|
|
|
|
new SimpleBroadcastReceiver(t -> restartObserver()),
|
2023-01-10 17:50:32 -08:00
|
|
|
getPackageFilter(mWellbeingProviderPkg,
|
2019-10-23 17:44:27 -07:00
|
|
|
Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED,
|
|
|
|
|
Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED,
|
2020-09-29 10:32:32 -07:00
|
|
|
Intent.ACTION_PACKAGE_RESTARTED),
|
|
|
|
|
null, mWorkerHandler);
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
|
|
|
|
|
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
|
|
|
|
|
filter.addDataScheme("package");
|
2020-09-29 10:32:32 -07:00
|
|
|
mContext.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged),
|
|
|
|
|
filter, null, mWorkerHandler);
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
restartObserver();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
|
|
|
|
private void onWellbeingUriChanged(Uri uri) {
|
|
|
|
|
Preconditions.assertNonUiThread();
|
2023-04-07 11:25:54 -07:00
|
|
|
updateAllPackages();
|
2020-09-29 10:32:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setInTest(boolean inTest) {
|
|
|
|
|
mIsInTest = inTest;
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
2019-10-23 17:44:27 -07:00
|
|
|
private void restartObserver() {
|
|
|
|
|
final ContentResolver resolver = mContext.getContentResolver();
|
|
|
|
|
resolver.unregisterContentObserver(mContentObserver);
|
2020-07-20 11:03:39 -07:00
|
|
|
Uri actionsUri = apiBuilder().path(PATH_ACTIONS).build();
|
2019-10-23 17:44:27 -07:00
|
|
|
try {
|
|
|
|
|
resolver.registerContentObserver(
|
|
|
|
|
actionsUri, true /* notifyForDescendants */, mContentObserver);
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
|
Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e);
|
|
|
|
|
if (mIsInTest) throw new RuntimeException(e);
|
|
|
|
|
}
|
2020-09-29 10:32:32 -07:00
|
|
|
updateAllPackages();
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainThread
|
|
|
|
|
private SystemShortcut getShortcutForApp(String packageName, int userId,
|
2024-04-11 04:27:23 +00:00
|
|
|
Context context, ItemInfo info, View originalView) {
|
2019-10-23 17:44:27 -07:00
|
|
|
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()
|
|
|
|
|
+ "'");
|
|
|
|
|
}
|
2024-04-11 04:27:23 +00:00
|
|
|
return new RemoteActionShortcut(action, context, info, originalView);
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Uri.Builder apiBuilder() {
|
|
|
|
|
return new Uri.Builder()
|
|
|
|
|
.scheme(SCHEME_CONTENT)
|
|
|
|
|
.authority(mWellbeingProviderPkg + ".api");
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
|
|
|
|
private boolean updateActions(String[] packageNames) {
|
2019-10-23 17:44:27 -07:00
|
|
|
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);
|
2020-05-29 13:38:44 -07:00
|
|
|
if (!remoteActionBundle.getBoolean(EXTRA_SUCCESS, true)) return false;
|
2019-12-06 11:08:56 -08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-10-23 17:44:27 -07:00
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
|
|
|
|
private void updateActionsWithRetry(int retryCount, @Nullable String packageName) {
|
2021-05-24 16:42:07 -07:00
|
|
|
if (DEBUG || mIsInTest) {
|
|
|
|
|
Log.i(TAG,
|
|
|
|
|
"updateActionsWithRetry(); retryCount: " + retryCount + ", package: "
|
|
|
|
|
+ packageName);
|
|
|
|
|
}
|
2020-09-29 10:32:32 -07:00
|
|
|
String[] packageNames = TextUtils.isEmpty(packageName)
|
2021-05-24 16:42:07 -07:00
|
|
|
? mContext.getSystemService(LauncherApps.class)
|
2020-09-29 10:32:32 -07:00
|
|
|
.getActivityList(null, Process.myUserHandle()).stream()
|
|
|
|
|
.map(li -> li.getApplicationInfo().packageName).distinct()
|
|
|
|
|
.toArray(String[]::new)
|
2021-05-24 16:42:07 -07:00
|
|
|
: new String[]{packageName};
|
2020-09-29 10:32:32 -07:00
|
|
|
|
|
|
|
|
mWorkerHandler.removeCallbacksAndMessages(packageName);
|
|
|
|
|
if (updateActions(packageNames)) {
|
|
|
|
|
return;
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
if (retryCount >= RETRY_TIMES_MS.length) {
|
|
|
|
|
// To many retries, skip
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-09-29 10:32:32 -07:00
|
|
|
mWorkerHandler.postDelayed(
|
2021-05-24 16:42:07 -07:00
|
|
|
() -> {
|
|
|
|
|
if (DEBUG || mIsInTest) Log.i(TAG, "Retrying; attempt " + (retryCount + 1));
|
|
|
|
|
updateActionsWithRetry(retryCount + 1, packageName);
|
|
|
|
|
},
|
2020-09-29 10:32:32 -07:00
|
|
|
packageName, RETRY_TIMES_MS[retryCount]);
|
|
|
|
|
}
|
2019-10-23 17:44:27 -07:00
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
|
|
|
|
private void updateAllPackages() {
|
2021-05-24 16:42:07 -07:00
|
|
|
if (DEBUG || mIsInTest) Log.i(TAG, "updateAllPackages");
|
2020-09-29 10:32:32 -07:00
|
|
|
updateActionsWithRetry(0, null);
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
|
2020-09-29 10:32:32 -07:00
|
|
|
@WorkerThread
|
2019-10-23 17:44:27 -07:00
|
|
|
private void onAppPackageChanged(Intent intent) {
|
|
|
|
|
if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]");
|
2020-09-29 10:32:32 -07:00
|
|
|
Preconditions.assertNonUiThread();
|
2019-10-23 17:44:27 -07:00
|
|
|
|
|
|
|
|
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)) {
|
2020-09-29 10:32:32 -07:00
|
|
|
mWorkerHandler.removeCallbacksAndMessages(packageName);
|
|
|
|
|
synchronized (mModelLock) {
|
|
|
|
|
mPackageToActionId.remove(packageName);
|
|
|
|
|
}
|
2019-10-23 17:44:27 -07:00
|
|
|
} else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
|
2020-09-29 10:32:32 -07:00
|
|
|
updateActionsWithRetry(0, packageName);
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shortcut factory for generating wellbeing action
|
|
|
|
|
*/
|
2024-04-11 04:27:23 +00:00
|
|
|
public static final SystemShortcut.Factory<ActivityContext> SHORTCUT_FACTORY =
|
|
|
|
|
(context, info, originalView) ->
|
|
|
|
|
(info.getTargetComponent() == null) ? null
|
|
|
|
|
: INSTANCE.get(originalView.getContext()).getShortcutForApp(
|
|
|
|
|
info.getTargetComponent().getPackageName(), info.user.getIdentifier(),
|
|
|
|
|
ActivityContext.lookupContext(originalView.getContext()),
|
|
|
|
|
info, originalView);
|
2019-10-23 17:44:27 -07:00
|
|
|
}
|