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

Commit 3fef1f28 authored by Robin Lee's avatar Robin Lee
Browse files

Bounce work challenge through a WorkLockActivity

This stops us from depending on Settings for keeping the work profile
secure. Instead that is delegated to a smaller Activity inside SystemUI
which has just two jobs:

 1) Don't let anyone see the content that's supposed to be locked.

 2) Start ConfirmCredentialsActivity (still lives in Settings for now)

Bug: 31001762
Test: //cts/hostsidetests/devicepolicy/src/com/android/cts/devicepolicy/MixedManagedProfileOwnerTest#testResetPasswordFbe
Test: //tests/PoApi/src/com/google/android/afwtest/poapi/WorkChallengeTest
Change-Id: If43820b683007a60a37edf32fb65b442a8fb709b
parent 54402aab
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -483,6 +483,21 @@
            android:exported="true"
            android:enabled="@bool/config_enableKeyguardService" />

        <activity android:name=".keyguard.WorkLockActivity"
                  android:label="@string/accessibility_desc_work_lock"
                  android:permission="android.permission.MANAGE_USERS"
                  android:exported="false"
                  android:launchMode="singleTop"
                  android:excludeFromRecents="true"
                  android:stateNotNeeded="true"
                  android:resumeWhilePausing="true"
                  android:theme="@android:style/Theme.Black.NoTitleBar">
            <intent-filter>
                <action android:name="android.app.action.CONFIRM_DEVICE_CREDENTIAL_WITH_USER" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity android:name=".Somnambulator"
            android:label="@string/start_dreams"
            android:icon="@mipmap/ic_launcher_dreams"
+2 −0
Original line number Diff line number Diff line
@@ -460,6 +460,8 @@
    <string name="accessibility_desc_settings">Settings</string>
    <!-- Content description for the recent apps panel (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_recent_apps">Overview.</string>
    <!-- Content description for the graphic shown instead of an activity window while the activity is locked (not shown on the screen). [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_work_lock">Work lock screen</string>
    <!-- Content description for the close button in the zen mode panel introduction message. [CHAR LIMIT=NONE] -->
    <string name="accessibility_desc_close">Close</string>

+159 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.keyguard;

import static android.app.ActivityManager.TaskDescription;

import android.annotation.ColorInt;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;

/**
 * Bouncer between work activities and the activity used to confirm credentials before unlocking
 * a managed profile.
 * <p>
 * Shows a solid color when started, based on the organization color of the user it is supposed to
 * be blocking. Once focused, it switches to a screen to confirm credentials and auto-dismisses if
 * credentials are accepted.
 */
public class WorkLockActivity extends Activity {
    private static final String TAG = "WorkLockActivity";

    /**
     * ID of the locked user that this activity blocks access to.
     */
    @UserIdInt
    private int mUserId;

    /**
     * {@see KeyguardManager}
     */
    private KeyguardManager mKgm;

    /**
     * {@see DevicePolicyManager}
     */
    private DevicePolicyManager mDpm;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mUserId = getIntent().getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
        mDpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
        mKgm = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);

        final IntentFilter lockFilter = new IntentFilter();
        lockFilter.addAction(Intent.ACTION_DEVICE_LOCKED_CHANGED);
        registerReceiverAsUser(mLockEventReceiver, UserHandle.ALL, lockFilter,
                /* permission */ null, /* scheduler */ null);

        // Once the receiver is registered, check whether anything happened between now and the time
        // when this activity was launched. If it did and the user is unlocked now, just quit.
        if (!mKgm.isDeviceLocked(mUserId)) {
            finish();
            return;
        }

        // Get the organization color; this is a 24-bit integer provided by a DPC, guaranteed to
        // be completely opaque.
        final @ColorInt int color = mDpm.getOrganizationColorForUser(mUserId);

        // Draw captions overlaid on the content view, so the whole window is one solid color.
        setOverlayWithDecorCaptionEnabled(true);

        // Match task description to the task stack we are replacing so it's still recognizably the
        // original task stack with the same icon and title text.
        setTaskDescription(new TaskDescription(null, null, color));

        // Blank out the activity. When it is on-screen it will look like a Recents thumbnail with
        // redaction switched on.
        final View blankView = new View(this);
        blankView.setBackgroundColor(color);
        setContentView(blankView);

        // Respond to input events by showing the prompt to confirm credentials.
        blankView.setOnClickListener((View v) -> {
            showConfirmCredentialActivity();
        });
    }

    @Override
    public void onDestroy() {
        unregisterReceiver(mLockEventReceiver);
        super.onDestroy();
    }

    @Override
    public void onBackPressed() {
        // Ignore back presses.
        return;
    }

    private final BroadcastReceiver mLockEventReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, mUserId);
            if (userId == mUserId && !mKgm.isDeviceLocked(mUserId)) {
                finish();
            }
        }
    };

    private void showConfirmCredentialActivity() {
        if (isFinishing() || !mKgm.isDeviceLocked(mUserId)) {
            // Don't show the confirm credentials screen if we are already unlocked / unlocking.
            return;
        }

        final Intent credential = mKgm.createConfirmDeviceCredentialIntent(null, null, mUserId);
        if (credential == null) {
            return;
        }

        final ActivityOptions options = ActivityOptions.makeBasic();
        options.setLaunchTaskId(getTaskId());

        // Bring this activity back to the foreground after confirming credentials.
        final PendingIntent target = PendingIntent.getActivity(this, /* request */ -1, getIntent(),
                PendingIntent.FLAG_CANCEL_CURRENT |
                PendingIntent.FLAG_ONE_SHOT |
                PendingIntent.FLAG_IMMUTABLE, options.toBundle());

        credential.putExtra(Intent.EXTRA_INTENT, target.getIntentSender());
        try {
            ActivityManager.getService().startConfirmDeviceCredentialIntent(credential);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to start confirm credential intent", e);
        }
    }
}
+8 −8
Original line number Diff line number Diff line
@@ -11837,23 +11837,23 @@ public class ActivityManagerService extends IActivityManager.Stub
        }
        synchronized (this) {
            if (mStackSupervisor.isUserLockedProfile(userId)) {
            final long ident = Binder.clearCallingIdentity();
            try {
                if (mUserController.shouldConfirmCredentials(userId)) {
                    final int currentUserId = mUserController.getCurrentUserIdLocked();
                    if (mUserController.isLockScreenDisabled(currentUserId)) {
                        // If there is no device lock, we will show the profile's credential page.
                        mActivityStarter.showConfirmDeviceCredential(userId);
                    if (!mKeyguardController.isKeyguardLocked()) {
                        // If the device is not locked, we will prompt for credentials immediately.
                        mStackSupervisor.lockAllProfileTasks(userId);
                    } else {
                        // Showing launcher to avoid user entering credential twice.
                        startHomeActivityLocked(currentUserId, "notifyLockedProfile");
                    }
                }
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    }
    }
    @Override
    public void startConfirmDeviceCredentialIntent(Intent intent) {
+41 −33
Original line number Diff line number Diff line
@@ -763,43 +763,51 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
    }

    /**
     * TODO: Handle freefom mode.
     * @return true when credential confirmation is needed for the user and there is any
     *         activity started by the user in any visible stack.
     * Detects whether we should show a lock screen in front of this task for a locked user.
     * <p>
     * We'll do this if either of the following holds:
     * <ul>
     *   <li>The top activity explicitly belongs to {@param userId}.</li>
     *   <li>The top activity returns a result to an activity belonging to {@param userId}.</li>
     * </ul>
     *
     * @return {@code true} if the top activity looks like it belongs to {@param userId}.
     */
    boolean isUserLockedProfile(@UserIdInt int userId) {
        if (!mService.mUserController.shouldConfirmCredentials(userId)) {
            return false;
        }
        final ActivityStack fullScreenStack = getStack(FULLSCREEN_WORKSPACE_STACK_ID);
        final ActivityStack dockedStack = getStack(DOCKED_STACK_ID);
        final ActivityStack[] activityStacks = new ActivityStack[] {fullScreenStack, dockedStack};
        for (final ActivityStack activityStack : activityStacks) {
            if (activityStack == null) {
                continue;
            }
            if (activityStack.topRunningActivityLocked() == null) {
                continue;
            }
            if (activityStack.getStackVisibilityLocked(null) == STACK_INVISIBLE) {
                continue;
            }
            if (activityStack.isDockedStack() && mIsDockMinimized) {
                continue;
    private boolean taskTopActivityIsUser(TaskRecord task, @UserIdInt int userId) {
        // To handle the case that work app is in the task but just is not the top one.
        final ActivityRecord activityRecord = task.getTopActivity();
        final ActivityRecord resultTo = (activityRecord != null ? activityRecord.resultTo : null);

        return (activityRecord != null && activityRecord.userId == userId)
                || (resultTo != null && resultTo.userId == userId);
    }
            final TaskRecord topTask = activityStack.topTask();
            if (topTask == null) {
                continue;

    /**
     * Find all visible task stacks containing {@param userId} and intercept them with an activity
     * to block out the contents and possibly start a credential-confirming intent.
     *
     * @param userId user handle for the locked managed profile.
     */
    void lockAllProfileTasks(@UserIdInt int userId) {
        mWindowManager.deferSurfaceLayout();
        try {
            final List<ActivityStack> stacks = getStacks();
            for (int stackNdx = stacks.size() - 1; stackNdx >= 0; stackNdx--) {
                final List<TaskRecord> tasks = stacks.get(stackNdx).getAllTasks();
                for (int taskNdx = tasks.size() - 1; taskNdx >= 0; taskNdx--) {
                    final TaskRecord task = tasks.get(taskNdx);

                    // Check the task for a top activity belonging to userId, or returning a result
                    // to an activity belonging to userId. Example case: a document picker for
                    // personal files, opened by a work app, should still get locked.
                    if (taskTopActivityIsUser(task, userId)) {
                        mService.mActivityStarter.startTaskLockedActivity(task);
                    }
            // To handle the case that work app is in the task but just is not the top one.
            for (int i = topTask.mActivities.size() - 1; i >= 0; i--) {
                final ActivityRecord activityRecord = topTask.mActivities.get(i);
                if (activityRecord.userId == userId) {
                    return true;
                }
            }
        } finally {
            mWindowManager.continueSurfaceLayout();
        }
        return false;
    }

    void setNextTaskIdForUserLocked(int taskId, int userId) {
Loading