diff options
4 files changed, 196 insertions, 19 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java index d0b7ad3e9dd5..394949297d6d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java @@ -27,9 +27,15 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.os.IBinder; +import android.os.UserHandle; +import android.os.UserManager; +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.statusbar.IAppClipsService; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Application; @@ -37,6 +43,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.wm.shell.bubbles.Bubbles; import java.util.Optional; +import java.util.concurrent.ExecutionException; import javax.inject.Inject; @@ -46,21 +53,63 @@ import javax.inject.Inject; */ public class AppClipsService extends Service { + private static final String TAG = AppClipsService.class.getSimpleName(); + @Application private final Context mContext; private final FeatureFlags mFeatureFlags; private final Optional<Bubbles> mOptionalBubbles; private final DevicePolicyManager mDevicePolicyManager; + private final UserManager mUserManager; + private final boolean mAreTaskAndTimeIndependentPrerequisitesMet; + @VisibleForTesting() + @Nullable ServiceConnector<IAppClipsService> mProxyConnectorToMainProfile; + @Inject public AppClipsService(@Application Context context, FeatureFlags featureFlags, - Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) { + Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager, + UserManager userManager) { mContext = context; mFeatureFlags = featureFlags; mOptionalBubbles = optionalBubbles; mDevicePolicyManager = devicePolicyManager; + mUserManager = userManager; + + // The consumer of this service are apps that call through StatusBarManager API to query if + // it can use app clips API. Since these apps can be launched as work profile users, this + // service will start as work profile user. SysUI doesn't share injected instances for + // different users. This is why the bubbles instance injected will be incorrect. As the apps + // don't generally have permission to connect to a service running as different user, we + // start a proxy connection to communicate with the main user's version of this service. + if (mUserManager.isManagedProfile()) { + // No need to check for prerequisites in this case as those are incorrect for work + // profile user instance of the service and the main user version of the service will + // take care of this check. + mAreTaskAndTimeIndependentPrerequisitesMet = false; + + // Get the main user so that we can connect to the main user's version of the service. + UserHandle mainUser = mUserManager.getMainUser(); + if (mainUser == null) { + // If main user is not available there isn't much we can do, no apps can use app + // clips. + return; + } + + // Set up the connection to be used later during onBind callback. + mProxyConnectorToMainProfile = + new ServiceConnector.Impl<>( + context, + new Intent(context, AppClipsService.class), + Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY + | Context.BIND_NOT_VISIBLE, + mainUser.getIdentifier(), + IAppClipsService.Stub::asInterface); + return; + } mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables(); + mProxyConnectorToMainProfile = null; } private boolean checkIndependentVariables() { @@ -95,6 +144,13 @@ public class AppClipsService extends Service { return new IAppClipsService.Stub() { @Override public boolean canLaunchCaptureContentActivityForNote(int taskId) { + // In case of managed profile, use the main user's instance of the service. Callers + // cannot directly connect to the main user's instance as they may not have the + // permission to interact across users. + if (mUserManager.isManagedProfile()) { + return canLaunchCaptureContentActivityForNoteFromMainUser(taskId); + } + if (!mAreTaskAndTimeIndependentPrerequisitesMet) { return false; } @@ -107,4 +163,21 @@ public class AppClipsService extends Service { } }; } + + /** Returns whether the app clips API can be used by querying the service as the main user. */ + private boolean canLaunchCaptureContentActivityForNoteFromMainUser(int taskId) { + if (mProxyConnectorToMainProfile == null) { + return false; + } + + try { + AndroidFuture<Boolean> future = mProxyConnectorToMainProfile.postForResult( + service -> service.canLaunchCaptureContentActivityForNote(taskId)); + return future.get(); + } catch (ExecutionException | InterruptedException e) { + Log.d(TAG, "Exception from service\n" + e); + } + + return false; + } } 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 3cb1a34a921c..0487cbc995dd 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java @@ -40,6 +40,8 @@ import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.ResultReceiver; +import android.os.UserHandle; +import android.os.UserManager; import android.util.Log; import androidx.annotation.Nullable; @@ -79,13 +81,10 @@ public class AppClipsTrampolineActivity extends Activity { private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName(); static final String PERMISSION_SELF = "com.android.systemui.permission.SELF"; - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; + static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI"; static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE"; - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER"; - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public static final String EXTRA_CALLING_PACKAGE_NAME = TAG + "CALLING_PACKAGE_NAME"; + 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 DevicePolicyManager mDevicePolicyManager; @@ -95,6 +94,7 @@ public class AppClipsTrampolineActivity extends Activity { private final PackageManager mPackageManager; private final UserTracker mUserTracker; private final UiEventLogger mUiEventLogger; + private final UserManager mUserManager; private final ResultReceiver mResultReceiver; private Intent mKillAppClipsBroadcastIntent; @@ -103,7 +103,7 @@ public class AppClipsTrampolineActivity extends Activity { public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags, Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController, PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger, - @Main Handler mainHandler) { + UserManager userManager, @Main Handler mainHandler) { mDevicePolicyManager = devicePolicyManager; mFeatureFlags = flags; mOptionalBubbles = optionalBubbles; @@ -111,6 +111,7 @@ public class AppClipsTrampolineActivity extends Activity { mPackageManager = packageManager; mUserTracker = userTracker; mUiEventLogger = uiEventLogger; + mUserManager = userManager; mResultReceiver = createResultReceiver(mainHandler); } @@ -123,6 +124,12 @@ public class AppClipsTrampolineActivity extends Activity { return; } + if (mUserManager.isManagedProfile()) { + maybeStartActivityForWPUser(); + finish(); + return; + } + if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) { finish(); return; @@ -191,6 +198,19 @@ public class AppClipsTrampolineActivity extends Activity { } } + private void maybeStartActivityForWPUser() { + UserHandle mainUser = mUserManager.getMainUser(); + if (mainUser == null) { + setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED); + return; + } + + // Start the activity as the main user with activity result forwarding. + startActivityAsUser( + new Intent(this, AppClipsTrampolineActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT), mainUser); + } + private void setErrorResultAndFinish(int errorCode) { setResult(RESULT_OK, new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java index b55fe3677256..67b1099c1e0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java @@ -20,8 +20,10 @@ import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.admin.DevicePolicyManager; @@ -29,6 +31,8 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; import androidx.test.runner.AndroidJUnit4; @@ -42,6 +46,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -58,6 +63,9 @@ public final class AppClipsServiceTest extends SysuiTestCase { @Mock private Optional<Bubbles> mOptionalBubbles; @Mock private Bubbles mBubbles; @Mock private DevicePolicyManager mDevicePolicyManager; + @Mock private UserManager mUserManager; + + private AppClipsService mAppClipsService; @Before public void setUp() { @@ -119,26 +127,53 @@ public final class AppClipsServiceTest extends SysuiTestCase { @Test public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException { + mockToSatisfyAllPrerequisites(); + + assertThat(getInterfaceWithRealContext() + .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isTrue(); + } + + @Test + public void isManagedProfile_shouldUseProxyConnection() throws RemoteException { + when(mUserManager.isManagedProfile()).thenReturn(true); + when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); + IAppClipsService service = getInterfaceWithRealContext(); + mAppClipsService.mProxyConnectorToMainProfile = + Mockito.spy(mAppClipsService.mProxyConnectorToMainProfile); + + service.canLaunchCaptureContentActivityForNote(FAKE_TASK_ID); + + verify(mAppClipsService.mProxyConnectorToMainProfile).postForResult(any()); + } + + @Test + public void isManagedProfile_noMainUser_shouldReturnFalse() { + when(mUserManager.isManagedProfile()).thenReturn(true); + when(mUserManager.getMainUser()).thenReturn(null); + + getInterfaceWithRealContext(); + + assertThat(mAppClipsService.mProxyConnectorToMainProfile).isNull(); + } + + private void mockToSatisfyAllPrerequisites() { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); when(mOptionalBubbles.get()).thenReturn(mBubbles); when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true); when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false); - - assertThat(getInterfaceWithRealContext() - .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isTrue(); } private IAppClipsService getInterfaceWithRealContext() { - AppClipsService appClipsService = new AppClipsService(getContext(), mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager); - return getInterfaceFromService(appClipsService); + mAppClipsService = new AppClipsService(getContext(), mFeatureFlags, + mOptionalBubbles, mDevicePolicyManager, mUserManager); + return getInterfaceFromService(mAppClipsService); } private IAppClipsService getInterfaceWithMockContext() { - AppClipsService appClipsService = new AppClipsService(mMockContext, mFeatureFlags, - mOptionalBubbles, mDevicePolicyManager); - return getInterfaceFromService(appClipsService); + mAppClipsService = new AppClipsService(mMockContext, mFeatureFlags, + mOptionalBubbles, mDevicePolicyManager, mUserManager); + return getInterfaceFromService(mAppClipsService); } private static IAppClipsService getInterfaceFromService(AppClipsService appClipsService) { 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 ad06dcc6f327..31a33d4ff908 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 @@ -49,6 +49,8 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.UserHandle; +import android.os.UserManager; import android.testing.AndroidTestingRunner; import androidx.test.rule.ActivityTestRule; @@ -98,6 +100,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { private UserTracker mUserTracker; @Mock private UiEventLogger mUiEventLogger; + @Mock + private UserManager mUserManager; + @Main private Handler mMainHandler; @@ -109,7 +114,7 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { protected AppClipsTrampolineActivityTestable create(Intent unUsed) { return new AppClipsTrampolineActivityTestable(mDevicePolicyManager, mFeatureFlags, mOptionalBubbles, mNoteTaskController, mPackageManager, - mUserTracker, mUiEventLogger, mMainHandler); + mUserTracker, mUiEventLogger, mUserManager, mMainHandler); } }; @@ -264,6 +269,40 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); } + @Test + public void startAppClipsActivity_throughWPUser_shouldStartMainUserActivity() + throws NameNotFoundException { + when(mUserManager.isManagedProfile()).thenReturn(true); + when(mUserManager.getMainUser()).thenReturn(UserHandle.SYSTEM); + mockToSatisfyAllPrerequisites(); + + AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); + waitForIdleSync(); + + Intent actualIntent = activity.mStartedIntent; + assertThat(actualIntent.getComponent()).isEqualTo( + new ComponentName(mContext, AppClipsTrampolineActivity.class)); + assertThat(actualIntent.getFlags()).isEqualTo(Intent.FLAG_ACTIVITY_FORWARD_RESULT); + assertThat(activity.mStartingUser).isEqualTo(UserHandle.SYSTEM); + } + + @Test + public void startAppClipsActivity_throughWPUser_noMainUser_shouldFinishWithFailed() + throws NameNotFoundException { + when(mUserManager.isManagedProfile()).thenReturn(true); + when(mUserManager.getMainUser()).thenReturn(null); + + mockToSatisfyAllPrerequisites(); + + mActivityRule.launchActivity(mActivityIntent); + waitForIdleSync(); + + ActivityResult actualResult = mActivityRule.getActivityResult(); + assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); + assertThat(getStatusCodeExtra(actualResult.getResultData())) + .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); + } + private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true); when(mOptionalBubbles.isEmpty()).thenReturn(false); @@ -282,6 +321,9 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { public static final class AppClipsTrampolineActivityTestable extends AppClipsTrampolineActivity { + Intent mStartedIntent; + UserHandle mStartingUser; + public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager, FeatureFlags flags, Optional<Bubbles> optionalBubbles, @@ -289,9 +331,10 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { PackageManager packageManager, UserTracker userTracker, UiEventLogger uiEventLogger, + UserManager userManager, @Main Handler mainHandler) { super(devicePolicyManager, flags, optionalBubbles, noteTaskController, packageManager, - userTracker, uiEventLogger, mainHandler); + userTracker, uiEventLogger, userManager, mainHandler); } @Override @@ -303,6 +346,12 @@ public final class AppClipsTrampolineActivityTest extends SysuiTestCase { public void startActivity(Intent unUsed) { // Ignore this intent to avoid App Clips screenshot editing activity from starting. } + + @Override + public void startActivityAsUser(Intent startedIntent, UserHandle startingUser) { + mStartedIntent = startedIntent; + mStartingUser = startingUser; + } } private static int getStatusCodeExtra(Intent intent) { |