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