1 /* 2 * Copyright (C) 2023 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.systemui.screenshot.appclips; 18 19 import static android.content.Intent.ACTION_MAIN; 20 import static android.content.Intent.ACTION_VIEW; 21 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; 22 import static android.content.Intent.CATEGORY_LAUNCHER; 23 24 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; 25 26 import android.app.IActivityTaskManager; 27 import android.app.TaskInfo; 28 import android.app.WindowConfiguration; 29 import android.app.assist.AssistContent; 30 import android.content.ClipData; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.PackageManager; 35 import android.content.pm.ResolveInfo; 36 import android.graphics.Bitmap; 37 import android.graphics.HardwareRenderer; 38 import android.graphics.RecordingCanvas; 39 import android.graphics.Rect; 40 import android.graphics.RenderNode; 41 import android.graphics.drawable.Drawable; 42 import android.net.Uri; 43 import android.os.UserHandle; 44 import android.util.Log; 45 import android.view.Display; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 import androidx.lifecycle.LiveData; 50 import androidx.lifecycle.MutableLiveData; 51 import androidx.lifecycle.ViewModel; 52 import androidx.lifecycle.ViewModelProvider; 53 54 import com.android.systemui.dagger.qualifiers.Application; 55 import com.android.systemui.dagger.qualifiers.Background; 56 import com.android.systemui.dagger.qualifiers.Main; 57 import com.android.systemui.log.DebugLogger; 58 import com.android.systemui.screenshot.AssistContentRequester; 59 import com.android.systemui.screenshot.ImageExporter; 60 import com.android.systemui.screenshot.appclips.InternalBacklinksData.BacklinksData; 61 import com.android.systemui.screenshot.appclips.InternalBacklinksData.CrossProfileError; 62 63 import com.google.common.util.concurrent.FutureCallback; 64 import com.google.common.util.concurrent.Futures; 65 import com.google.common.util.concurrent.ListenableFuture; 66 import com.google.common.util.concurrent.SettableFuture; 67 68 import java.util.Collections; 69 import java.util.List; 70 import java.util.Set; 71 import java.util.UUID; 72 import java.util.concurrent.CancellationException; 73 import java.util.concurrent.ExecutionException; 74 import java.util.concurrent.Executor; 75 import java.util.concurrent.TimeUnit; 76 77 import javax.inject.Inject; 78 79 /** A {@link ViewModel} to help with the App Clips screenshot flow. */ 80 final class AppClipsViewModel extends ViewModel { 81 82 private static final String TAG = AppClipsViewModel.class.getSimpleName(); 83 84 private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; 85 private final ImageExporter mImageExporter; 86 private final IActivityTaskManager mAtmService; 87 private final AssistContentRequester mAssistContentRequester; 88 @Application private final Context mContext; 89 90 @Main 91 private final Executor mMainExecutor; 92 @Background 93 private final Executor mBgExecutor; 94 95 private final MutableLiveData<Bitmap> mScreenshotLiveData; 96 private final MutableLiveData<Uri> mResultLiveData; 97 private final MutableLiveData<Integer> mErrorLiveData; 98 private final MutableLiveData<List<InternalBacklinksData>> mBacklinksLiveData; 99 final MutableLiveData<InternalBacklinksData> mSelectedBacklinksLiveData; 100 AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, IActivityTaskManager atmService, AssistContentRequester assistContentRequester, @Application Context context, @Main Executor mainExecutor, @Background Executor bgExecutor)101 private AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper, 102 ImageExporter imageExporter, IActivityTaskManager atmService, 103 AssistContentRequester assistContentRequester, @Application Context context, 104 @Main Executor mainExecutor, @Background Executor bgExecutor) { 105 mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; 106 mImageExporter = imageExporter; 107 mAtmService = atmService; 108 mAssistContentRequester = assistContentRequester; 109 mContext = context; 110 mMainExecutor = mainExecutor; 111 mBgExecutor = bgExecutor; 112 113 mScreenshotLiveData = new MutableLiveData<>(); 114 mResultLiveData = new MutableLiveData<>(); 115 mErrorLiveData = new MutableLiveData<>(); 116 mBacklinksLiveData = new MutableLiveData<>(); 117 mSelectedBacklinksLiveData = new MutableLiveData<>(); 118 } 119 120 /** 121 * Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link #getScreenshot()}. 122 * 123 * @param displayId id of the {@link Display} to capture screenshot. 124 */ performScreenshot(int displayId)125 void performScreenshot(int displayId) { 126 mBgExecutor.execute(() -> { 127 Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot(displayId); 128 mMainExecutor.execute(() -> { 129 if (screenshot == null) { 130 mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED); 131 } else { 132 mScreenshotLiveData.setValue(screenshot); 133 } 134 }); 135 }); 136 } 137 138 /** 139 * Triggers the Backlinks flow which: 140 * <ul> 141 * <li>Evaluates the tasks to query. 142 * <li>Requests {@link AssistContent} from all valid tasks. 143 * <li>Transforms {@link AssistContent} into {@link InternalBacklinksData} for Backlinks. 144 * <li>The {@link InternalBacklinksData}s are reported to activity via 145 * {@link #getBacklinksLiveData()}. 146 * </ul> 147 * 148 * @param taskIdsToIgnore id of the tasks to ignore when querying for {@link AssistContent} 149 * @param displayId id of the display to query tasks for Backlinks data 150 */ triggerBacklinks(Set<Integer> taskIdsToIgnore, int displayId)151 void triggerBacklinks(Set<Integer> taskIdsToIgnore, int displayId) { 152 DebugLogger.INSTANCE.logcatMessage(this, () -> "Backlinks triggered"); 153 ListenableFuture<List<InternalBacklinksData>> backlinksData = getAllAvailableBacklinks( 154 taskIdsToIgnore, displayId); 155 Futures.addCallback(backlinksData, new FutureCallback<>() { 156 @Override 157 public void onSuccess(@Nullable List<InternalBacklinksData> result) { 158 if (result != null && !result.isEmpty()) { 159 // Set the list of backlinks before setting the selected backlink as this is 160 // required when updating the backlink data text view. 161 mBacklinksLiveData.setValue(result); 162 mSelectedBacklinksLiveData.setValue(result.get(0)); 163 } 164 } 165 166 @Override 167 public void onFailure(Throwable t) { 168 Log.e(TAG, "Error querying for Backlinks data", t); 169 } 170 }, mMainExecutor); 171 } 172 173 /** Returns a {@link LiveData} that holds the captured screenshot. */ getScreenshot()174 LiveData<Bitmap> getScreenshot() { 175 return mScreenshotLiveData; 176 } 177 178 /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */ getResultLiveData()179 LiveData<Uri> getResultLiveData() { 180 return mResultLiveData; 181 } 182 183 /** 184 * Returns a {@link LiveData} that holds the error codes for 185 * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}. 186 */ getErrorLiveData()187 LiveData<Integer> getErrorLiveData() { 188 return mErrorLiveData; 189 } 190 191 /** 192 * Returns a {@link LiveData} that holds all the available Backlinks data and the currently 193 * selected index for displaying the Backlinks in the UI. 194 */ getBacklinksLiveData()195 LiveData<List<InternalBacklinksData>> getBacklinksLiveData() { 196 return mBacklinksLiveData; 197 } 198 199 /** 200 * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to 201 * {@link LiveData}. 202 */ saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user)203 void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user) { 204 mBgExecutor.execute(() -> { 205 // Render the screenshot bitmap in background. 206 Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds); 207 208 // Export and save the screenshot in background. 209 ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(mBgExecutor, 210 UUID.randomUUID(), screenshotBitmap, user, Display.DEFAULT_DISPLAY); 211 212 // Get the result and update state on main thread. 213 exportFuture.addListener(() -> { 214 try { 215 ImageExporter.Result result = exportFuture.get(); 216 if (result.uri == null) { 217 mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED); 218 return; 219 } 220 221 mResultLiveData.setValue(result.uri); 222 } catch (CancellationException | InterruptedException | ExecutionException e) { 223 mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED); 224 } 225 }, mMainExecutor); 226 }); 227 } 228 renderBitmap(Drawable drawable, Rect bounds)229 private static Bitmap renderBitmap(Drawable drawable, Rect bounds) { 230 final RenderNode output = new RenderNode("Screenshot save"); 231 output.setPosition(0, 0, bounds.width(), bounds.height()); 232 RecordingCanvas canvas = output.beginRecording(); 233 canvas.translate(-bounds.left, -bounds.top); 234 canvas.clipRect(bounds); 235 drawable.draw(canvas); 236 output.endRecording(); 237 return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height()); 238 } 239 getAllAvailableBacklinks( Set<Integer> taskIdsToIgnore, int displayId)240 private ListenableFuture<List<InternalBacklinksData>> getAllAvailableBacklinks( 241 Set<Integer> taskIdsToIgnore, int displayId) { 242 ListenableFuture<List<TaskInfo>> allTasksOnDisplayFuture = getAllTasksOnDisplay(displayId); 243 244 ListenableFuture<List<ListenableFuture<InternalBacklinksData>>> backlinksNestedListFuture = 245 Futures.transform(allTasksOnDisplayFuture, allTasksOnDisplay -> 246 allTasksOnDisplay 247 .stream() 248 .filter(taskInfo -> shouldIncludeTask(taskInfo, taskIdsToIgnore)) 249 .map(taskInfo -> new InternalTaskInfo(taskInfo.topActivityInfo, 250 taskInfo.taskId, taskInfo.userId, 251 getPackageManagerForUser(taskInfo.userId))) 252 .map(this::getBacklinksDataForTaskInfo) 253 .toList(), 254 mBgExecutor); 255 256 return Futures.transformAsync(backlinksNestedListFuture, Futures::allAsList, mBgExecutor); 257 } 258 getPackageManagerForUser(int userId)259 private PackageManager getPackageManagerForUser(int userId) { 260 // If app clips was launched as the same user, then reuse the available PM from mContext. 261 if (mContext.getUserId() == userId) { 262 return mContext.getPackageManager(); 263 } 264 265 // PackageManager required for a different user, create its context and return its PM. 266 UserHandle userHandle = UserHandle.of(userId); 267 return mContext.createContextAsUser(userHandle, /* flags= */ 0).getPackageManager(); 268 } 269 270 /** 271 * Returns all tasks on a given display after querying {@link IActivityTaskManager} from the 272 * {@link #mBgExecutor}. 273 */ getAllTasksOnDisplay(int displayId)274 private ListenableFuture<List<TaskInfo>> getAllTasksOnDisplay(int displayId) { 275 SettableFuture<List<TaskInfo>> recentTasksFuture = SettableFuture.create(); 276 mBgExecutor.execute(() -> { 277 try { 278 // Directly call into ActivityTaskManagerService instead of going through WMShell 279 // because WMShell is only available in the main SysUI process and App Clips runs 280 // in its own separate process as it deals with bitmaps. 281 List<TaskInfo> allTasksOnDisplay = mAtmService.getTasks( 282 /* maxNum= */ Integer.MAX_VALUE, 283 // PIP tasks are not visible in recents. So _not_ filtering for 284 // tasks that are only visible in recents. 285 /* filterOnlyVisibleRecents= */ false, 286 /* keepIntentExtra= */ false, 287 displayId) 288 .stream() 289 .map(runningTaskInfo -> (TaskInfo) runningTaskInfo) 290 .toList(); 291 recentTasksFuture.set(allTasksOnDisplay); 292 } catch (Exception e) { 293 Log.e(TAG, String.format("Error getting all tasks on displayId %d", displayId), e); 294 recentTasksFuture.set(Collections.emptyList()); 295 } 296 }); 297 298 return withTimeout(recentTasksFuture); 299 } 300 301 /** 302 * Returns whether the app represented by the provided {@link TaskInfo} should be included for 303 * querying for {@link AssistContent}. 304 * 305 * <p>This does not check whether the task has a launcher icon. 306 */ shouldIncludeTask(TaskInfo taskInfo, Set<Integer> taskIdsToIgnore)307 private boolean shouldIncludeTask(TaskInfo taskInfo, Set<Integer> taskIdsToIgnore) { 308 DebugLogger.INSTANCE.logcatMessage(this, 309 () -> String.format("shouldIncludeTask taskId %d; topActivity %s", taskInfo.taskId, 310 taskInfo.topActivity)); 311 312 // Only consider tasks that shouldn't be ignored, are visible, and running. Furthermore, 313 // types such as launcher/home/dock/assistant are ignored. 314 return !taskIdsToIgnore.contains(taskInfo.taskId) 315 && taskInfo.isVisible 316 && taskInfo.isRunning 317 && taskInfo.numActivities > 0 318 && taskInfo.topActivity != null 319 && taskInfo.topActivityInfo != null 320 && taskInfo.getActivityType() == WindowConfiguration.ACTIVITY_TYPE_STANDARD; 321 } 322 323 /** 324 * Returns an {@link InternalBacklinksData} that represents the Backlink data internally, which 325 * is captured by querying the system using {@link TaskInfo#taskId}. 326 */ getBacklinksDataForTaskInfo( InternalTaskInfo internalTaskInfo)327 private ListenableFuture<InternalBacklinksData> getBacklinksDataForTaskInfo( 328 InternalTaskInfo internalTaskInfo) { 329 DebugLogger.INSTANCE.logcatMessage(this, 330 () -> String.format("getBacklinksDataForTaskId for taskId %d; topActivity %s", 331 internalTaskInfo.getTaskId(), 332 internalTaskInfo.getTopActivityNameForDebugLogging())); 333 334 // Unlike other SysUI components, App Clips is started by the notes app so it runs as the 335 // same user as the notes app. That is, if the notes app was running as work profile user 336 // then App Clips also runs as work profile user. This is why while checking for user of the 337 // screenshotted app the check is performed using UserHandle.myUserId instead of using the 338 // more complex UserTracker. 339 if (internalTaskInfo.getUserId() != UserHandle.myUserId()) { 340 return getCrossProfileErrorBacklinkForTask(internalTaskInfo); 341 } 342 343 SettableFuture<InternalBacklinksData> backlinksData = SettableFuture.create(); 344 int taskId = internalTaskInfo.getTaskId(); 345 mAssistContentRequester.requestAssistContent(taskId, assistContent -> backlinksData.set( 346 getBacklinksDataFromAssistContent(internalTaskInfo, assistContent))); 347 return withTimeout(backlinksData); 348 } 349 getCrossProfileErrorBacklinkForTask( InternalTaskInfo internalTaskInfo)350 private ListenableFuture<InternalBacklinksData> getCrossProfileErrorBacklinkForTask( 351 InternalTaskInfo internalTaskInfo) { 352 String appName = internalTaskInfo.getTopActivityAppName(); 353 Drawable appIcon = internalTaskInfo.getTopActivityAppIcon(); 354 InternalBacklinksData errorData = new CrossProfileError(appIcon, appName); 355 return Futures.immediateFuture(errorData); 356 } 357 358 /** Returns the same {@link ListenableFuture} but with a 5 {@link TimeUnit#SECONDS} timeout. */ withTimeout(ListenableFuture<V> future)359 private static <V> ListenableFuture<V> withTimeout(ListenableFuture<V> future) { 360 return Futures.withTimeout(future, 5L, TimeUnit.SECONDS, 361 newSingleThreadScheduledExecutor()); 362 } 363 364 /** 365 * A utility method to get {@link InternalBacklinksData} to use for Backlinks functionality from 366 * {@link AssistContent} received from the app whose screenshot is taken. 367 * 368 * <p>There are multiple ways an app can provide deep-linkable data via {@link AssistContent} 369 * but Backlinks restricts to using only one way. The following is the ordered list based on 370 * preference: 371 * <ul> 372 * <li>{@link AssistContent#getWebUri()} is the most preferred way. 373 * <li>Second preference is given to {@link AssistContent#getIntent()} when the app provides 374 * the intent, see {@link AssistContent#isAppProvidedIntent()}. 375 * <li>The last preference is given to an {@link Intent} that is built using 376 * {@link Intent#ACTION_MAIN} and {@link Intent#CATEGORY_LAUNCHER}. 377 * </ul> 378 * 379 * @param internalTaskInfo {@link InternalTaskInfo} of the task which provided the 380 * {@link AssistContent}. 381 * @param content the {@link AssistContent} to map into Backlinks {@link ClipData}. 382 * @return {@link InternalBacklinksData} that represents the Backlinks data along with app icon. 383 */ getBacklinksDataFromAssistContent( InternalTaskInfo internalTaskInfo, @Nullable AssistContent content)384 private InternalBacklinksData getBacklinksDataFromAssistContent( 385 InternalTaskInfo internalTaskInfo, 386 @Nullable AssistContent content) { 387 DebugLogger.INSTANCE.logcatMessage(this, 388 () -> String.format("getBacklinksDataFromAssistContent taskId %d; topActivity %s", 389 internalTaskInfo.getTaskId(), 390 internalTaskInfo.getTopActivityNameForDebugLogging())); 391 392 String screenshottedAppName = internalTaskInfo.getTopActivityAppName(); 393 Drawable screenshottedAppIcon = internalTaskInfo.getTopActivityAppIcon(); 394 Intent screenshottedAppMainLauncherIntent = getMainLauncherIntentForTask( 395 internalTaskInfo.getTopActivityPackageName(), internalTaskInfo.getPackageManager()); 396 ClipData screenshottedAppMainLauncherClipData = 397 ClipData.newIntent(screenshottedAppName, screenshottedAppMainLauncherIntent); 398 InternalBacklinksData fallback = 399 new BacklinksData(screenshottedAppMainLauncherClipData, screenshottedAppIcon); 400 if (content == null) { 401 return fallback; 402 } 403 404 // First preference is given to app provided uri. 405 if (content.isAppProvidedWebUri()) { 406 DebugLogger.INSTANCE.logcatMessage(this, 407 () -> "getBacklinksDataFromAssistContent: app has provided a uri"); 408 409 Uri uri = content.getWebUri(); 410 Intent backlinksIntent = new Intent(ACTION_VIEW).setData(uri); 411 BacklinkDisplayInfo backlinkDisplayInfo = getInfoThatResolvesIntent(backlinksIntent, 412 internalTaskInfo); 413 if (backlinkDisplayInfo != null) { 414 DebugLogger.INSTANCE.logcatMessage(this, 415 () -> "getBacklinksDataFromAssistContent: using app provided uri"); 416 return new BacklinksData( 417 ClipData.newRawUri(backlinkDisplayInfo.getDisplayLabel(), uri), 418 backlinkDisplayInfo.getAppIcon()); 419 } 420 } 421 422 // Second preference is given to app provided, hopefully deep-linking, intent. 423 if (content.isAppProvidedIntent()) { 424 DebugLogger.INSTANCE.logcatMessage(this, 425 () -> "getBacklinksDataFromAssistContent: app has provided an intent"); 426 427 Intent backlinksIntent = content.getIntent(); 428 BacklinkDisplayInfo backlinkDisplayInfo = getInfoThatResolvesIntent(backlinksIntent, 429 internalTaskInfo); 430 if (backlinkDisplayInfo != null) { 431 DebugLogger.INSTANCE.logcatMessage(this, 432 () -> "getBacklinksDataFromAssistContent: using app provided intent"); 433 return new BacklinksData( 434 ClipData.newIntent(backlinkDisplayInfo.getDisplayLabel(), backlinksIntent), 435 backlinkDisplayInfo.getAppIcon()); 436 } 437 } 438 439 DebugLogger.INSTANCE.logcatMessage(this, 440 () -> "getBacklinksDataFromAssistContent: using fallback"); 441 return fallback; 442 } 443 444 /** 445 * Returns {@link BacklinkDisplayInfo} for the app that would resolve the provided backlink 446 * {@link Intent}. 447 * 448 * <p>The method uses the {@link PackageManager} available in the provided 449 * {@link InternalTaskInfo}. 450 * 451 * <p>This method returns {@code null} if Android is not able to resolve the backlink intent or 452 * if the resolved app does not have an icon in the launcher. 453 */ 454 @Nullable getInfoThatResolvesIntent(Intent backlinkIntent, InternalTaskInfo internalTaskInfo)455 private BacklinkDisplayInfo getInfoThatResolvesIntent(Intent backlinkIntent, 456 InternalTaskInfo internalTaskInfo) { 457 PackageManager packageManager = internalTaskInfo.getPackageManager(); 458 459 // Query for all available activities as there is a chance that multiple apps could resolve 460 // the intent. In such cases the normal `intent.resolveActivity` API returns the activity 461 // resolver info which isn't helpful for further checks. Also, using MATCH_DEFAULT_ONLY flag 462 // is required as that flag will be used when the notes app builds the intent and calls 463 // startActivity with the intent. 464 List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(backlinkIntent, 465 PackageManager.MATCH_DEFAULT_ONLY); 466 if (resolveInfos.isEmpty()) { 467 DebugLogger.INSTANCE.logcatMessage(this, 468 () -> "getInfoThatResolvesIntent: could not resolve backlink intent"); 469 return null; 470 } 471 472 // Only use the first result as the list is ordered from best match to worst and Android 473 // will also use the best match with `intent.startActivity` API which notes app will use. 474 ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; 475 if (activityInfo == null) { 476 DebugLogger.INSTANCE.logcatMessage(this, 477 () -> "getInfoThatResolvesIntent: could not find activity info for backlink " 478 + "intent"); 479 return null; 480 } 481 482 // Ignore resolved backlink app if users cannot start it through all apps tray. 483 if (!canAppStartThroughLauncher(activityInfo.packageName, packageManager)) { 484 DebugLogger.INSTANCE.logcatMessage(this, 485 () -> "getInfoThatResolvesIntent: ignoring resolved backlink app as it cannot" 486 + " start through launcher"); 487 return null; 488 } 489 490 Drawable appIcon = InternalBacklinksDataKt.getAppIcon(activityInfo, packageManager); 491 String appName = InternalBacklinksDataKt.getAppName(activityInfo, packageManager); 492 return new BacklinkDisplayInfo(appIcon, appName); 493 } 494 495 /** 496 * Returns whether the app represented by the provided {@code pkgName} can be launched through 497 * the all apps tray by the user. 498 */ canAppStartThroughLauncher(String pkgName, PackageManager pkgManager)499 private static boolean canAppStartThroughLauncher(String pkgName, PackageManager pkgManager) { 500 // Use Intent.resolveActivity API to check if the intent resolves as that is what Android 501 // uses internally when apps use Context.startActivity. 502 return getMainLauncherIntentForTask(pkgName, pkgManager) 503 .resolveActivity(pkgManager) != null; 504 } 505 getMainLauncherIntentForTask(String pkgName, PackageManager packageManager)506 private static Intent getMainLauncherIntentForTask(String pkgName, 507 PackageManager packageManager) { 508 Intent intent = new Intent(ACTION_MAIN).addCategory(CATEGORY_LAUNCHER).setPackage(pkgName); 509 510 // Not all apps use DEFAULT_CATEGORY for their main launcher activity so the exact component 511 // needs to be queried and set on the Intent in order for note-taking apps to be able to 512 // start this intent. When starting an activity with an implicit intent, Android adds the 513 // DEFAULT_CATEGORY flag otherwise it fails to resolve the intent. 514 ResolveInfo resolvedActivity = packageManager.resolveActivity(intent, /* flags= */ 0); 515 if (resolvedActivity != null) { 516 intent.setComponent(resolvedActivity.getComponentInfo().getComponentName()); 517 } 518 519 return intent; 520 } 521 522 /** Helper factory to help with injecting {@link AppClipsViewModel}. */ 523 static final class Factory implements ViewModelProvider.Factory { 524 525 private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; 526 private final ImageExporter mImageExporter; 527 private final IActivityTaskManager mAtmService; 528 private final AssistContentRequester mAssistContentRequester; 529 @Application private final Context mContext; 530 @Main 531 private final Executor mMainExecutor; 532 @Background 533 private final Executor mBgExecutor; 534 535 @Inject Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, IActivityTaskManager atmService, AssistContentRequester assistContentRequester, @Application Context context, @Main Executor mainExecutor, @Background Executor bgExecutor)536 Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper, ImageExporter imageExporter, 537 IActivityTaskManager atmService, AssistContentRequester assistContentRequester, 538 @Application Context context, @Main Executor mainExecutor, 539 @Background Executor bgExecutor) { 540 mAppClipsCrossProcessHelper = appClipsCrossProcessHelper; 541 mImageExporter = imageExporter; 542 mAtmService = atmService; 543 mAssistContentRequester = assistContentRequester; 544 mContext = context; 545 mMainExecutor = mainExecutor; 546 mBgExecutor = bgExecutor; 547 } 548 549 @NonNull 550 @Override create(@onNull Class<T> modelClass)551 public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { 552 if (modelClass != AppClipsViewModel.class) { 553 throw new IllegalArgumentException(); 554 } 555 556 //noinspection unchecked 557 return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter, 558 mAtmService, mAssistContentRequester, mContext, mMainExecutor, 559 mBgExecutor); 560 } 561 } 562 } 563