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

Commit 9cec481b authored by Ajinkya Chalke's avatar Ajinkya Chalke
Browse files

Support App Clips for WP user.

Bug:274913069
Test: atest AppClipsServiceTest AppClipsTrampolineActivityTest
Change-Id: I63df6f990fd0179741a92f32ae2f8573fae6762f
parent 9e90c675
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) {