Merge "Support App Clips for WP user." into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
4d93facc7f
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user