Merge "Implement work profile considerations in app clips" into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
5d63998d8f
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(() -> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user