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.Executors.MAIN_EXECUTOR; 22 import static com.android.launcher3.util.Executors.createAndStartNewLooper; 23 24 import android.annotation.TargetApi; 25 import android.app.RemoteAction; 26 import android.content.ContentProviderClient; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.content.pm.LauncherApps; 32 import android.database.ContentObserver; 33 import android.net.Uri; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.os.DeadObjectException; 37 import android.os.Handler; 38 import android.os.Message; 39 import android.os.Process; 40 import android.os.UserHandle; 41 import android.text.TextUtils; 42 import android.util.ArrayMap; 43 import android.util.Log; 44 45 import androidx.annotation.MainThread; 46 47 import com.android.launcher3.BaseDraggingActivity; 48 import com.android.launcher3.R; 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.MainThreadInitializedObject; 53 import com.android.launcher3.util.PackageManagerHelper; 54 import com.android.launcher3.util.Preconditions; 55 import com.android.launcher3.util.SimpleBroadcastReceiver; 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 @TargetApi(Build.VERSION_CODES.Q) 65 public final class WellbeingModel { 66 private static final String TAG = "WellbeingModel"; 67 private static final int[] RETRY_TIMES_MS = {5000, 15000, 30000}; 68 private static final boolean DEBUG = false; 69 70 private static final int MSG_PACKAGE_ADDED = 1; 71 private static final int MSG_PACKAGE_REMOVED = 2; 72 private static final int MSG_FULL_REFRESH = 3; 73 74 // Welbeing contract 75 private static final String METHOD_GET_ACTIONS = "get_actions"; 76 private static final String EXTRA_ACTIONS = "actions"; 77 private static final String EXTRA_ACTION = "action"; 78 private static final String EXTRA_MAX_NUM_ACTIONS_SHOWN = "max_num_actions_shown"; 79 private static final String EXTRA_PACKAGES = "packages"; 80 private static final String EXTRA_SUCCESS = "success"; 81 82 public static final MainThreadInitializedObject<WellbeingModel> INSTANCE = 83 new MainThreadInitializedObject<>(WellbeingModel::new); 84 85 private final Context mContext; 86 private final String mWellbeingProviderPkg; 87 private final Handler mWorkerHandler; 88 89 private final ContentObserver mContentObserver; 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 mWorkerHandler = 101 new Handler(createAndStartNewLooper("WellbeingHandler"), this::handleMessage); 102 103 mWellbeingProviderPkg = mContext.getString(R.string.wellbeing_provider_pkg); 104 mContentObserver = new ContentObserver(MAIN_EXECUTOR.getHandler()) { 105 @Override 106 public void onChange(boolean selfChange, Uri uri) { 107 // Wellbeing reports that app actions have changed. 108 if (DEBUG || mIsInTest) { 109 Log.d(TAG, "ContentObserver.onChange() called with: selfChange = [" + selfChange 110 + "], uri = [" + uri + "]"); 111 } 112 Preconditions.assertUIThread(); 113 updateWellbeingData(); 114 } 115 }; 116 117 if (!TextUtils.isEmpty(mWellbeingProviderPkg)) { 118 context.registerReceiver( 119 new SimpleBroadcastReceiver(this::onWellbeingProviderChanged), 120 PackageManagerHelper.getPackageFilter(mWellbeingProviderPkg, 121 Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_CHANGED, 122 Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_DATA_CLEARED, 123 Intent.ACTION_PACKAGE_RESTARTED)); 124 125 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 126 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 127 filter.addDataScheme("package"); 128 context.registerReceiver(new SimpleBroadcastReceiver(this::onAppPackageChanged), 129 filter); 130 131 restartObserver(); 132 } 133 } 134 setInTest(boolean inTest)135 public void setInTest(boolean inTest) { 136 mIsInTest = inTest; 137 } 138 onWellbeingProviderChanged(Intent intent)139 protected void onWellbeingProviderChanged(Intent intent) { 140 if (DEBUG || mIsInTest) { 141 Log.d(TAG, "Changes to Wellbeing package: intent = [" + intent + "]"); 142 } 143 restartObserver(); 144 } 145 restartObserver()146 private void restartObserver() { 147 final ContentResolver resolver = mContext.getContentResolver(); 148 resolver.unregisterContentObserver(mContentObserver); 149 Uri actionsUri = apiBuilder().path("actions").build(); 150 try { 151 resolver.registerContentObserver( 152 actionsUri, true /* notifyForDescendants */, mContentObserver); 153 } catch (Exception e) { 154 Log.e(TAG, "Failed to register content observer for " + actionsUri + ": " + e); 155 if (mIsInTest) throw new RuntimeException(e); 156 } 157 updateWellbeingData(); 158 } 159 160 @MainThread getShortcutForApp(String packageName, int userId, BaseDraggingActivity activity, ItemInfo info)161 private SystemShortcut getShortcutForApp(String packageName, int userId, 162 BaseDraggingActivity activity, ItemInfo info) { 163 Preconditions.assertUIThread(); 164 // Work profile apps are not recognized by digital wellbeing. 165 if (userId != UserHandle.myUserId()) { 166 if (DEBUG || mIsInTest) { 167 Log.d(TAG, "getShortcutForApp [" + packageName + "]: not current user"); 168 } 169 return null; 170 } 171 172 synchronized (mModelLock) { 173 String actionId = mPackageToActionId.get(packageName); 174 final RemoteAction action = actionId != null ? mActionIdMap.get(actionId) : null; 175 if (action == null) { 176 if (DEBUG || mIsInTest) { 177 Log.d(TAG, "getShortcutForApp [" + packageName + "]: no action"); 178 } 179 return null; 180 } 181 if (DEBUG || mIsInTest) { 182 Log.d(TAG, 183 "getShortcutForApp [" + packageName + "]: action: '" + action.getTitle() 184 + "'"); 185 } 186 return new RemoteActionShortcut(action, activity, info); 187 } 188 } 189 updateWellbeingData()190 private void updateWellbeingData() { 191 mWorkerHandler.sendEmptyMessage(MSG_FULL_REFRESH); 192 } 193 apiBuilder()194 private Uri.Builder apiBuilder() { 195 return new Uri.Builder() 196 .scheme(SCHEME_CONTENT) 197 .authority(mWellbeingProviderPkg + ".api"); 198 } 199 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 handleMessage(Message msg)263 private boolean handleMessage(Message msg) { 264 switch (msg.what) { 265 case MSG_PACKAGE_REMOVED: { 266 String packageName = (String) msg.obj; 267 mWorkerHandler.removeCallbacksAndMessages(packageName); 268 synchronized (mModelLock) { 269 mPackageToActionId.remove(packageName); 270 } 271 return true; 272 } 273 case MSG_PACKAGE_ADDED: { 274 String packageName = (String) msg.obj; 275 mWorkerHandler.removeCallbacksAndMessages(packageName); 276 if (!updateActions(packageName)) { 277 scheduleRefreshRetry(msg); 278 } 279 return true; 280 } 281 282 case MSG_FULL_REFRESH: { 283 // Remove all existing messages 284 mWorkerHandler.removeCallbacksAndMessages(null); 285 final String[] packageNames = mContext.getSystemService(LauncherApps.class) 286 .getActivityList(null, Process.myUserHandle()).stream() 287 .map(li -> li.getApplicationInfo().packageName).distinct() 288 .toArray(String[]::new); 289 if (!updateActions(packageNames)) { 290 scheduleRefreshRetry(msg); 291 } 292 return true; 293 } 294 } 295 return false; 296 } 297 scheduleRefreshRetry(Message originalMsg)298 private void scheduleRefreshRetry(Message originalMsg) { 299 int retryCount = originalMsg.arg1; 300 if (retryCount >= RETRY_TIMES_MS.length) { 301 // To many retries, skip 302 return; 303 } 304 305 Message msg = Message.obtain(originalMsg); 306 msg.arg1 = retryCount + 1; 307 mWorkerHandler.sendMessageDelayed(msg, RETRY_TIMES_MS[retryCount]); 308 } 309 onAppPackageChanged(Intent intent)310 private void onAppPackageChanged(Intent intent) { 311 if (DEBUG || mIsInTest) Log.d(TAG, "Changes in apps: intent = [" + intent + "]"); 312 Preconditions.assertUIThread(); 313 314 final String packageName = intent.getData().getSchemeSpecificPart(); 315 if (packageName == null || packageName.length() == 0) { 316 // they sent us a bad intent 317 return; 318 } 319 320 final String action = intent.getAction(); 321 if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { 322 Message.obtain(mWorkerHandler, MSG_PACKAGE_REMOVED, packageName).sendToTarget(); 323 } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { 324 Message.obtain(mWorkerHandler, MSG_PACKAGE_ADDED, packageName).sendToTarget(); 325 } 326 } 327 328 /** 329 * Shortcut factory for generating wellbeing action 330 */ 331 public static final SystemShortcut.Factory SHORTCUT_FACTORY = 332 (activity, info) -> (info.getTargetComponent() == null) ? null : INSTANCE.get(activity) 333 .getShortcutForApp( 334 info.getTargetComponent().getPackageName(), info.user.getIdentifier(), 335 activity, info); 336 } 337