diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 8c01bae43c3db..5ad16f0a60784 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -293,10 +293,9 @@ public class ImageExporter { final ContentValues values = createMetadata(time, format, fileName); Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - if (UserHandle.myUserId() != owner.getIdentifier()) { - baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); - } - Uri uri = resolver.insert(baseUri, values); + Uri uriWithUserId = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); + + Uri uri = resolver.insert(uriWithUserId, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java index 72613249552a4..2f96f6c656c4a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java @@ -247,7 +247,7 @@ public class AppClipsActivity extends ComponentActivity { } updateImageDimensions(); - mViewModel.saveScreenshotThenFinish(drawable, bounds); + mViewModel.saveScreenshotThenFinish(drawable, bounds, getUser()); } private void setResultThenFinish(Uri uri) { @@ -255,6 +255,11 @@ public class AppClipsActivity extends ComponentActivity { return; } + // Grant permission here instead of in the trampoline activity because this activity can run + // as work profile user so the URI can belong to the work profile user while the trampoline + // activity always runs as main user. + grantUriPermission(mCallingPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + Bundle data = new Bundle(); data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java index e1619dc9b6eef..afc8bff91766b 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java @@ -19,7 +19,7 @@ package com.android.systemui.screenshot.appclips; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; -import android.view.Display; +import android.os.UserManager; import androidx.annotation.Nullable; @@ -27,6 +27,7 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.infra.ServiceConnector; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.settings.DisplayTracker; import javax.inject.Inject; @@ -35,14 +36,20 @@ import javax.inject.Inject; class AppClipsCrossProcessHelper { private final ServiceConnector mProxyConnector; + private final DisplayTracker mDisplayTracker; @Inject - AppClipsCrossProcessHelper(@Application Context context) { - mProxyConnector = new ServiceConnector.Impl(context, + AppClipsCrossProcessHelper(@Application Context context, UserManager userManager, + DisplayTracker displayTracker) { + // Start a service as main user so that even if the app clips activity is running as work + // profile user the service is able to use correct instance of Bubbles to grab a screenshot + // excluding the bubble layer. + mProxyConnector = new ServiceConnector.Impl<>(context, new Intent(context, AppClipsScreenshotHelperService.class), Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY - | Context.BIND_NOT_VISIBLE, context.getUserId(), + | Context.BIND_NOT_VISIBLE, userManager.getMainUser().getIdentifier(), IAppClipsScreenshotHelperService.Stub::asInterface); + mDisplayTracker = displayTracker; } /** @@ -56,7 +63,9 @@ class AppClipsCrossProcessHelper { try { AndroidFuture future = mProxyConnector.postForResult( - service -> service.takeScreenshot(Display.DEFAULT_DISPLAY)); + service -> + // Take a screenshot of the default display of the user. + service.takeScreenshot(mDisplayTracker.getDefaultDisplayId())); return future.get().createBitmapThenCloseBuffer(); } catch (Exception e) { return null; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java index 0487cbc995dd3..f00803c6d64bc 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -21,7 +21,6 @@ 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 android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; @@ -34,6 +33,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserInfo; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; @@ -82,6 +82,8 @@ 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"; + @VisibleForTesting + static final String EXTRA_USE_WP_USER = TAG + "USE_WP_USER"; 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"; @@ -98,6 +100,7 @@ public class AppClipsTrampolineActivity extends Activity { private final ResultReceiver mResultReceiver; private Intent mKillAppClipsBroadcastIntent; + private UserHandle mNotesAppUser; @Inject public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, @@ -165,15 +168,21 @@ public class AppClipsTrampolineActivity extends Activity { return; } + mNotesAppUser = getUser(); + if (getIntent().getBooleanExtra(EXTRA_USE_WP_USER, /* defaultValue= */ false)) { + // Get the work profile user internally instead of passing around via intent extras as + // this activity is exported apps could potentially mess around with intent extras. + mNotesAppUser = getWorkProfileUser().orElse(mNotesAppUser); + } + 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 { - // Start the App Clips activity. - startActivity(intent); + // Start the App Clips activity for the user corresponding to the notes app user. + startActivityAsUser(intent, mNotesAppUser); // Set up the broadcast intent that will inform the above App Clips activity to finish // when this trampoline activity is finished. @@ -198,6 +207,13 @@ public class AppClipsTrampolineActivity extends Activity { } } + private Optional getWorkProfileUser() { + return mUserTracker.getUserProfiles().stream() + .filter(profile -> mUserManager.isManagedProfile(profile.id)) + .findFirst() + .map(UserInfo::getUserHandle); + } + private void maybeStartActivityForWPUser() { UserHandle mainUser = mUserManager.getMainUser(); if (mainUser == null) { @@ -205,9 +221,13 @@ public class AppClipsTrampolineActivity extends Activity { return; } - // Start the activity as the main user with activity result forwarding. + // Start the activity as the main user with activity result forwarding. Set the intent extra + // so that the newly started trampoline activity starts the actual app clips activity as the + // work profile user. Starting the app clips activity as the work profile user is required + // to save the screenshot in work profile user storage and grant read permission to the URI. startActivityAsUser( new Intent(this, AppClipsTrampolineActivity.class) + .putExtra(EXTRA_USE_WP_USER, /* value= */ true) .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); } @@ -221,7 +241,7 @@ public class AppClipsTrampolineActivity extends Activity { int callingPackageUid = 0; try { callingPackageUid = mPackageManager.getApplicationInfoAsUser(callingPackageName, - APPLICATION_INFO_FLAGS, mUserTracker.getUserId()).uid; + APPLICATION_INFO_FLAGS, mNotesAppUser.getIdentifier()).uid; } catch (NameNotFoundException e) { Log.d(TAG, "Couldn't find notes app UID " + e); } @@ -254,14 +274,14 @@ public class AppClipsTrampolineActivity extends Activity { if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class); - convertedData.setData(uri).addFlags(FLAG_GRANT_READ_URI_PERMISSION); + convertedData.setData(uri); } // Broadcast no longer required, setting it to null. mKillAppClipsBroadcastIntent = null; // Expand the note bubble before returning the result. - mNoteTaskController.showNoteTask(NoteTaskEntryPoint.APP_CLIPS); + mNoteTaskController.showNoteTaskAsUser(NoteTaskEntryPoint.APP_CLIPS, mNotesAppUser); setResult(RESULT_OK, convertedData); finish(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java index 4cbca28a40322..b0e4cc978ae2e 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.graphics.RenderNode; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Process; +import android.os.UserHandle; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; @@ -110,16 +110,14 @@ final class AppClipsViewModel extends ViewModel { * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to * {@link LiveData}. */ - void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) { + void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds, UserHandle user) { mBgExecutor.execute(() -> { // Render the screenshot bitmap in background. Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds); // Export and save the screenshot in background. - // TODO(b/267310185): Save to work profile UserHandle. ListenableFuture exportFuture = mImageExporter.export( - mBgExecutor, UUID.randomUUID(), screenshotBitmap, - Process.myUserHandle()); + mBgExecutor, UUID.randomUUID(), screenshotBitmap, user); // Get the result and update state on main thread. exportFuture.addListener(() -> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java index 3c08d58cbb674..27eec801ef621 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsActivityTest.java @@ -37,8 +37,8 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Parcel; +import android.os.Process; import android.os.ResultReceiver; -import android.os.UserHandle; import android.testing.AndroidTestingRunner; import android.widget.ImageView; @@ -120,7 +120,8 @@ public final class AppClipsActivityTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = TEST_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), any(Bitmap.class), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(Process.myUserHandle()))) + .thenReturn(Futures.immediateFuture(result)); } @After diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java index cbd9dba3cdbf6..e9007ff84f13e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java @@ -27,6 +27,7 @@ import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; +import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_USE_WP_USER; import static com.google.common.truth.Truth.assertThat; @@ -74,6 +75,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; import java.util.Optional; @RunWith(AndroidTestingRunner.class) @@ -82,7 +84,6 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private static final String TEST_URI_STRING = "www.test-uri.com"; private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING); private static final int TEST_UID = 42; - private static final int TEST_USER_ID = 43; private static final String TEST_CALLING_PACKAGE = "test-calling-package"; @Mock @@ -287,6 +288,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { assertThat(actualIntent.getComponent()).isEqualTo( new ComponentName(mContext, AppClipsTrampolineActivity.class)); assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + assertThat(actualIntent.getBooleanExtra(EXTRA_USE_WP_USER, false)).isTrue(); assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); } @@ -313,13 +315,13 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { when(mOptionalBubbles.get()).thenReturn(mBubbles); when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - when(mUserTracker.getUserId()).thenReturn(TEST_USER_ID); + when(mUserTracker.getUserProfiles()).thenReturn(List.of()); ApplicationInfo testApplicationInfo = new ApplicationInfo(); testApplicationInfo.uid = TEST_UID; when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), any(ApplicationInfoFlags.class), - eq(TEST_USER_ID))).thenReturn(testApplicationInfo); + eq(mContext.getUser().getIdentifier()))).thenReturn(testApplicationInfo); } public static final class AppClipsTrampolineActivityTestable extends diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java index e7c3c05786278..b7b8b11ba887f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java @@ -31,6 +31,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.net.Uri; +import android.os.Process; import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; @@ -57,10 +58,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { private static final Drawable FAKE_DRAWABLE = new ShapeDrawable(); private static final Rect FAKE_RECT = new Rect(); private static final Uri FAKE_URI = Uri.parse("www.test-uri.com"); + private static final UserHandle USER_HANDLE = Process.myUserHandle(); @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper; @Mock private ImageExporter mImageExporter; - private AppClipsViewModel mViewModel; @Before @@ -99,10 +100,10 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( + eq(USER_HANDLE))).thenReturn( Futures.immediateFailedFuture(new ExecutionException(new Throwable()))); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -113,10 +114,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { @Test public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() { when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn( - Futures.immediateFuture(new ImageExporter.Result())); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(new ImageExporter.Result())); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()) @@ -129,9 +129,9 @@ public final class AppClipsViewModelTest extends SysuiTestCase { ImageExporter.Result result = new ImageExporter.Result(); result.uri = FAKE_URI; when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), - any(UserHandle.class))).thenReturn(Futures.immediateFuture(result)); + eq(USER_HANDLE))).thenReturn(Futures.immediateFuture(result)); - mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT); + mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT, USER_HANDLE); waitForIdleSync(); assertThat(mViewModel.getErrorLiveData().getValue()).isNull();