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