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