• 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.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
20 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
21 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
22 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
23 import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
24 
25 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED;
26 
27 import android.app.Activity;
28 import android.content.ActivityNotFoundException;
29 import android.content.ClipData;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.Intent.CaptureContentForNoteStatusCodes;
34 import android.content.pm.PackageManager;
35 import android.content.pm.PackageManager.ApplicationInfoFlags;
36 import android.content.pm.PackageManager.NameNotFoundException;
37 import android.net.Uri;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.os.Parcel;
41 import android.os.ResultReceiver;
42 import android.os.UserHandle;
43 import android.util.Log;
44 
45 import androidx.annotation.Nullable;
46 import androidx.annotation.VisibleForTesting;
47 
48 import com.android.internal.infra.AndroidFuture;
49 import com.android.internal.infra.ServiceConnector;
50 import com.android.internal.logging.UiEventLogger;
51 import com.android.internal.statusbar.IAppClipsService;
52 import com.android.systemui.broadcast.BroadcastSender;
53 import com.android.systemui.dagger.qualifiers.Application;
54 import com.android.systemui.dagger.qualifiers.Background;
55 import com.android.systemui.dagger.qualifiers.Main;
56 import com.android.systemui.log.DebugLogger;
57 import com.android.systemui.notetask.NoteTaskController;
58 import com.android.systemui.notetask.NoteTaskEntryPoint;
59 import com.android.systemui.res.R;
60 
61 import java.util.concurrent.Executor;
62 
63 import javax.inject.Inject;
64 
65 /**
66  * A trampoline activity that is responsible for:
67  * <ul>
68  *     <li>Performing precondition checks before starting the actual screenshot activity.
69  *     <li>Communicating with the screenshot activity and the calling activity.
70  * </ul>
71  *
72  * <p>As this activity is started in a bubble app, the windowing for this activity is restricted
73  * to the parent bubble app. The screenshot editing activity, see {@link AppClipsActivity}, is
74  * started in a regular activity window using {@link Intent#FLAG_ACTIVITY_NEW_TASK}. However,
75  * {@link Activity#startActivityForResult(Intent, int)} is not compatible with
76  * {@link Intent#FLAG_ACTIVITY_NEW_TASK}. So, this activity acts as a trampoline activity to
77  * abstract the complexity of communication with the screenshot editing activity for a simpler
78  * developer experience.
79  *
80  * TODO(b/267309532): Polish UI and animations.
81  */
82 public class AppClipsTrampolineActivity extends Activity {
83 
84     private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
85     static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
86     static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
87     static final String EXTRA_CLIP_DATA = TAG + "CLIP_DATA";
88     static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
89     static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
90     static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME";
91     static final String EXTRA_CALLING_PACKAGE_TASK_ID = TAG + "CALLING_PACKAGE_TASK_ID";
92     private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0);
93 
94     private final NoteTaskController mNoteTaskController;
95     private final PackageManager mPackageManager;
96     private final UiEventLogger mUiEventLogger;
97     private final BroadcastSender mBroadcastSender;
98     @Background
99     private final Executor mBgExecutor;
100     @Main
101     private final Executor mMainExecutor;
102     private final ResultReceiver mResultReceiver;
103 
104     private final ServiceConnector<IAppClipsService> mAppClipsServiceConnector;
105 
106     private UserHandle mUserHandle;
107     private Intent mKillAppClipsBroadcastIntent;
108 
109     @Inject
AppClipsTrampolineActivity(@pplication Context context, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler)110     public AppClipsTrampolineActivity(@Application Context context,
111             NoteTaskController noteTaskController, PackageManager packageManager,
112             UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
113             @Background Executor bgExecutor, @Main Executor mainExecutor,
114             @Main Handler mainHandler) {
115         mNoteTaskController = noteTaskController;
116         mPackageManager = packageManager;
117         mUiEventLogger = uiEventLogger;
118         mBroadcastSender = broadcastSender;
119         mBgExecutor = bgExecutor;
120         mMainExecutor = mainExecutor;
121 
122         mResultReceiver = createResultReceiver(mainHandler);
123         mAppClipsServiceConnector = createServiceConnector(context);
124     }
125 
126     /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */
127     @VisibleForTesting
AppClipsTrampolineActivity(ServiceConnector<IAppClipsService> appClipsServiceConnector, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler)128     AppClipsTrampolineActivity(ServiceConnector<IAppClipsService> appClipsServiceConnector,
129             NoteTaskController noteTaskController, PackageManager packageManager,
130             UiEventLogger uiEventLogger, BroadcastSender broadcastSender,
131             @Background Executor bgExecutor, @Main Executor mainExecutor,
132             @Main Handler mainHandler) {
133         mAppClipsServiceConnector = appClipsServiceConnector;
134         mNoteTaskController = noteTaskController;
135         mPackageManager = packageManager;
136         mUiEventLogger = uiEventLogger;
137         mBroadcastSender = broadcastSender;
138         mBgExecutor = bgExecutor;
139         mMainExecutor = mainExecutor;
140 
141         mResultReceiver = createResultReceiver(mainHandler);
142     }
143 
144     @Override
onCreate(@ullable Bundle savedInstanceState)145     protected void onCreate(@Nullable Bundle savedInstanceState) {
146         super.onCreate(savedInstanceState);
147 
148         if (savedInstanceState != null) {
149             return;
150         }
151 
152         mUserHandle = getUser();
153 
154         mBgExecutor.execute(() -> {
155             AndroidFuture<Integer> statusCodeFuture = mAppClipsServiceConnector.postForResult(
156                     service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId()));
157             statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor);
158         });
159     }
160 
161     @Override
onDestroy()162     protected void onDestroy() {
163         if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
164             mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
165         }
166 
167         super.onDestroy();
168     }
169 
handleAppClipsStatusCode(@aptureContentForNoteStatusCodes int statusCode, Throwable error)170     private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode,
171             Throwable error) {
172         if (isFinishing()) {
173             // It's too late, trampoline activity is finishing or already finished. Return early.
174             return;
175         }
176 
177         if (error != null) {
178             Log.d(TAG, "Error querying app clips service", error);
179             setErrorResultAndFinish(statusCode);
180             return;
181         }
182 
183         switch (statusCode) {
184             case CAPTURE_CONTENT_FOR_NOTE_SUCCESS:
185                 launchAppClipsActivity();
186                 break;
187 
188             case CAPTURE_CONTENT_FOR_NOTE_FAILED:
189             case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED:
190             case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN:
191             default:
192                 setErrorResultAndFinish(statusCode);
193         }
194     }
195 
launchAppClipsActivity()196     private void launchAppClipsActivity() {
197         ComponentName componentName = ComponentName.unflattenFromString(
198                     getString(R.string.config_screenshotAppClipsActivityComponent));
199         String callingPackageName = getCallingPackage();
200         int callingPackageTaskId = getTaskId();
201 
202         Intent intent = new Intent()
203                 .setComponent(componentName)
204                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
205                 .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver)
206                 .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName)
207                 .putExtra(EXTRA_CALLING_PACKAGE_TASK_ID, callingPackageTaskId);
208         try {
209             startActivity(intent);
210 
211             // Set up the broadcast intent that will inform the above App Clips activity to finish
212             // when this trampoline activity is finished.
213             mKillAppClipsBroadcastIntent =
214                     new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
215                             .setComponent(componentName)
216                             .setPackage(componentName.getPackageName());
217 
218             // Log successful triggering of screenshot for notes.
219             logScreenshotTriggeredUiEvent(callingPackageName);
220         } catch (ActivityNotFoundException e) {
221             setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
222         }
223     }
224 
setErrorResultAndFinish(int errorCode)225     private void setErrorResultAndFinish(int errorCode) {
226         setResult(RESULT_OK,
227                 new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode));
228         finish();
229     }
230 
logScreenshotTriggeredUiEvent(@ullable String callingPackageName)231     private void logScreenshotTriggeredUiEvent(@Nullable String callingPackageName) {
232         int callingPackageUid = 0;
233         try {
234             callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName,
235                     APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid;
236         } catch (NameNotFoundException e) {
237             Log.d(TAG, "Couldn't find notes app UID " + e);
238         }
239 
240         mUiEventLogger.log(SCREENSHOT_FOR_NOTE_TRIGGERED, callingPackageUid, callingPackageName);
241     }
242 
243     private class AppClipsResultReceiver extends ResultReceiver {
244 
AppClipsResultReceiver(Handler handler)245         AppClipsResultReceiver(Handler handler) {
246             super(handler);
247         }
248 
249         @Override
onReceiveResult(int resultCode, Bundle resultData)250         protected void onReceiveResult(int resultCode, Bundle resultData) {
251             if (isFinishing()) {
252                 // It's too late, trampoline activity is finishing or already finished.
253                 // Return early.
254                 return;
255             }
256 
257             // Package the response that should be sent to the calling activity.
258             Intent convertedData = new Intent();
259             int statusCode = CAPTURE_CONTENT_FOR_NOTE_FAILED;
260             if (resultData != null) {
261                 statusCode = resultData.getInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
262                         CAPTURE_CONTENT_FOR_NOTE_FAILED);
263             }
264             convertedData.putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, statusCode);
265 
266             if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) {
267                 Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class);
268                 convertedData.setData(uri);
269 
270                 if (resultData.containsKey(EXTRA_CLIP_DATA)) {
271                     ClipData backlinksData = resultData.getParcelable(EXTRA_CLIP_DATA,
272                             ClipData.class);
273                     convertedData.setClipData(backlinksData);
274 
275                     DebugLogger.INSTANCE.logcatMessage(this,
276                             () -> "onReceiveResult: sending notes app ClipData");
277                 }
278             }
279 
280             // Broadcast no longer required, setting it to null.
281             mKillAppClipsBroadcastIntent = null;
282 
283             // Expand the note bubble before returning the result.
284             mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle);
285             setResult(RESULT_OK, convertedData);
286             finish();
287         }
288     }
289 
290     /**
291      * @return a {@link ResultReceiver} by initializing an {@link AppClipsResultReceiver} and
292      * converting it into a generic {@link ResultReceiver} to pass across a different but trusted
293      * process.
294      */
createResultReceiver(@ain Handler handler)295     private ResultReceiver createResultReceiver(@Main Handler handler) {
296         AppClipsResultReceiver appClipsResultReceiver = new AppClipsResultReceiver(handler);
297         Parcel parcel = Parcel.obtain();
298         appClipsResultReceiver.writeToParcel(parcel, 0);
299         parcel.setDataPosition(0);
300 
301         ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel);
302         parcel.recycle();
303         return resultReceiver;
304     }
305 
createServiceConnector( @pplication Context context)306     private ServiceConnector<IAppClipsService> createServiceConnector(
307             @Application Context context) {
308         return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class),
309                 Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE,
310                 UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface);
311     }
312 
313     /** This is a test only API for mocking response from {@link AppClipsActivity}. */
314     @VisibleForTesting
getResultReceiverForTest()315     public ResultReceiver getResultReceiverForTest() {
316         return mResultReceiver;
317     }
318 }
319