/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.screenshot.appclips; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.ResultReceiver; import android.os.UserHandle; import android.util.Log; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.notetask.NoteTaskController; import com.android.systemui.notetask.NoteTaskEntryPoint; import java.util.concurrent.Executor; import javax.inject.Inject; /** * A trampoline activity that is responsible for: * * *

As this activity is started in a bubble app, the windowing for this activity is restricted * to the parent bubble app. The screenshot editing activity, see {@link AppClipsActivity}, is * started in a regular activity window using {@link Intent#FLAG_ACTIVITY_NEW_TASK}. However, * {@link Activity#startActivityForResult(Intent, int)} is not compatible with * {@link Intent#FLAG_ACTIVITY_NEW_TASK}. So, this activity acts as a trampoline activity to * abstract the complexity of communication with the screenshot editing activity for a simpler * developer experience. * * TODO(b/267309532): Polish UI and animations. */ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; private static final ApplicationInfoFlags APPLICATION_INFO_FLAGS = ApplicationInfoFlags.of(0); private final NoteTaskController mNoteTaskController; private final PackageManager mPackageManager; private final UiEventLogger mUiEventLogger; private final BroadcastSender mBroadcastSender; @Background private final Executor mBgExecutor; @Main private final Executor mMainExecutor; private final ResultReceiver mResultReceiver; private final ServiceConnector mAppClipsServiceConnector; private UserHandle mUserHandle; private Intent mKillAppClipsBroadcastIntent; @Inject public AppClipsTrampolineActivity(@Application Context context, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler) { mNoteTaskController = noteTaskController; mPackageManager = packageManager; mUiEventLogger = uiEventLogger; mBroadcastSender = broadcastSender; mBgExecutor = bgExecutor; mMainExecutor = mainExecutor; mResultReceiver = createResultReceiver(mainHandler); mAppClipsServiceConnector = createServiceConnector(context); } /** A constructor used only for testing to verify interactions with {@link ServiceConnector}. */ @VisibleForTesting AppClipsTrampolineActivity(ServiceConnector appClipsServiceConnector, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler) { mAppClipsServiceConnector = appClipsServiceConnector; mNoteTaskController = noteTaskController; mPackageManager = packageManager; mUiEventLogger = uiEventLogger; mBroadcastSender = broadcastSender; mBgExecutor = bgExecutor; mMainExecutor = mainExecutor; mResultReceiver = createResultReceiver(mainHandler); } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { return; } mUserHandle = getUser(); mBgExecutor.execute(() -> { AndroidFuture statusCodeFuture = mAppClipsServiceConnector.postForResult( service -> service.canLaunchCaptureContentActivityForNoteInternal(getTaskId())); statusCodeFuture.whenCompleteAsync(this::handleAppClipsStatusCode, mMainExecutor); }); } @Override protected void onDestroy() { if (isFinishing() && mKillAppClipsBroadcastIntent != null) { mBroadcastSender.sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF); } super.onDestroy(); } private void handleAppClipsStatusCode(@CaptureContentForNoteStatusCodes int statusCode, Throwable error) { if (isFinishing()) { // It's too late, trampoline activity is finishing or already finished. Return early. return; } if (error != null) { Log.d(TAG, "Error querying app clips service", error); setErrorResultAndFinish(statusCode); return; } switch (statusCode) { case CAPTURE_CONTENT_FOR_NOTE_SUCCESS: launchAppClipsActivity(); break; case CAPTURE_CONTENT_FOR_NOTE_FAILED: case CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED: case CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN: default: setErrorResultAndFinish(statusCode); } } private void launchAppClipsActivity() { ComponentName componentName = ComponentName.unflattenFromString( getString(R.string.config_screenshotAppClipsActivityComponent)); String callingPackageName = getCallingPackage(); Intent intent = new Intent() .setComponent(componentName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver) .putExtra(EXTRA_CALLING_PACKAGE_NAME, callingPackageName); try { startActivity(intent); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. mKillAppClipsBroadcastIntent = new Intent(ACTION_FINISH_FROM_TRAMPOLINE) .setComponent(componentName) .setPackage(componentName.getPackageName()); // Log successful triggering of screenshot for notes. logScreenshotTriggeredUiEvent(callingPackageName); } catch (ActivityNotFoundException e) { setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); } } private void setErrorResultAndFinish(int errorCode) { setResult(RESULT_OK, new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode)); finish(); } private void logScreenshotTriggeredUiEvent(@Nullable String callingPackageName) { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, APPLICATION_INFO_FLAGS, mUserHandle.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } mUiEventLogger.log(SCREENSHOT_FOR_NOTE_TRIGGERED, callingPackageUid, callingPackageName); } private class AppClipsResultReceiver extends ResultReceiver { AppClipsResultReceiver(Handler handler) { super(handler); } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (isFinishing()) { // It's too late, trampoline activity is finishing or already finished. // Return early. return; } // Package the response that should be sent to the calling activity. Intent convertedData = new Intent(); int statusCode = CAPTURE_CONTENT_FOR_NOTE_FAILED; if (resultData != null) { statusCode = resultData.getInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_FAILED); } convertedData.putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, statusCode); if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class); convertedData.setData(uri); } // Broadcast no longer required, setting it to null. mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mUserHandle); setResult(RESULT_OK, convertedData); finish(); } } /** * @return a {@link ResultReceiver} by initializing an {@link AppClipsResultReceiver} and * converting it into a generic {@link ResultReceiver} to pass across a different but trusted * process. */ private ResultReceiver createResultReceiver(@Main Handler handler) { AppClipsResultReceiver appClipsResultReceiver = new AppClipsResultReceiver(handler); Parcel parcel = Parcel.obtain(); appClipsResultReceiver.writeToParcel(parcel, 0); parcel.setDataPosition(0); ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(parcel); parcel.recycle(); return resultReceiver; } private ServiceConnector createServiceConnector( @Application Context context) { return new ServiceConnector.Impl<>(context, new Intent(context, AppClipsService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY | Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM, IAppClipsService.Stub::asInterface); } /** This is a test only API for mocking response from {@link AppClipsActivity}. */ @VisibleForTesting public ResultReceiver getResultReceiverForTest() { return mResultReceiver; } }