Support App Clips for WP user.

Bug:274913069
Test: atest AppClipsServiceTest AppClipsTrampolineActivityTest
Change-Id: I63df6f990fd0179741a92f32ae2f8573fae6762f
This commit is contained in:
Ajinkya Chalke
2023-04-03 14:25:12 +00:00
parent 9e90c675bc
commit 9cec481b6d
4 changed files with 198 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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