Merge "Implement work profile considerations in app clips" into udc-dev

This commit is contained in:
Ajinkya Chalke
2023-05-12 05:43:34 +00:00
committed by Android (Google) Code Review
8 changed files with 70 additions and 36 deletions

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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<IAppClipsScreenshotHelperService> mProxyConnector;
private final DisplayTracker mDisplayTracker;
@Inject
AppClipsCrossProcessHelper(@Application Context context) {
mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(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<ScreenshotHardwareBufferInternal> 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;

View File

@@ -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<UserHandle> 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();
}

View File

@@ -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<ImageExporter.Result> 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(() -> {

View File

@@ -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

View File

@@ -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

View File

@@ -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();