• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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