Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4d93facc authored by Ajinkya Chalke's avatar Ajinkya Chalke Committed by Android (Google) Code Review
Browse files

Merge "Support App Clips for WP user." into udc-dev

parents 99e08bc3 9cec481b
Loading
Loading
Loading
Loading
+74 −1
Original line number Diff line number Diff line
@@ -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;
    }
}
+27 −7
Original line number Diff line number Diff line
@@ -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));
+44 −9
Original line number Diff line number Diff line
@@ -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) {
+51 −2
Original line number Diff line number Diff line
@@ -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) {