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

Commit 017fbda0 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[Screen share] Split system cast and share-to-app perm dialog classes.

For b/352327853, the UX for the system cast permission dialog and the
share-to-app permission dialog will diverge even further from where they
are now. We're currently using a single DialogDelegate class to show
both types of dialogs. This CL splits that class into two classes to
make the rest of b/352327853's implementation easier.

Bug: 352327853
Flag: NONE refactor
Test: Share screen to app -> verify dialog has correct strings for
single app and entire screen, start share from dialog
Test: Cast screen to other device -> verify dialog has correct strings
for single app and entire screen, start share from dialog
Test: atest SystemCastPermissionDialogDelegateTest
ShareToAppPermissionDialogDelegateTest
Change-Id: I49217daca7bc779d724492f3457c2479c8631cc1

Change-Id: Ie16de17ff4bdaba9ea21a4261fd3e49cac303cdd
parent 9fdb0e80
Loading
Loading
Loading
Loading
+39 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.mediaprojection

import android.content.pm.PackageManager
import com.android.systemui.util.Utils

/** Various utility methods related to media projection. */
object MediaProjectionUtils {
    /**
     * Returns true iff projecting to the given [packageName] means that we're casting media to a
     * *different* device (as opposed to sharing media to some application on *this* device).
     */
    fun packageHasCastingCapabilities(
        packageManager: PackageManager,
        packageName: String
    ): Boolean {
        // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
        // different device or the same device, because headless remote display packages are the
        // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
        // because it means that any projection by those headless remote display packages will be
        // marked as going to a different device, even if that isn't always true. See b/321078669.
        return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
    }
}
+46 −25
Original line number Diff line number Diff line
@@ -41,7 +41,6 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.media.projection.IMediaProjection;
import android.media.projection.MediaProjectionConfig;
import android.media.projection.MediaProjectionManager;
@@ -50,10 +49,8 @@ import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.text.BidiFormatter;
import android.text.SpannableString;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;
import android.view.Window;

@@ -61,6 +58,7 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger;
import com.android.systemui.mediaprojection.MediaProjectionServiceHelper;
import com.android.systemui.mediaprojection.MediaProjectionUtils;
import com.android.systemui.mediaprojection.SessionCreationSource;
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity;
import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver;
@@ -68,12 +66,13 @@ import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDi
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.AlertDialogWithDelegate;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.Utils;

import dagger.Lazy;
import java.util.function.Consumer;

import javax.inject.Inject;

import dagger.Lazy;

public class MediaProjectionPermissionActivity extends Activity
        implements DialogInterface.OnClickListener {
    private static final String TAG = "MediaProjectionPermissionActivity";
@@ -189,30 +188,14 @@ public class MediaProjectionPermissionActivity extends Activity

        final String appName = extractAppName(aInfo, packageManager);
        final boolean hasCastingCapabilities =
                Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName);
                MediaProjectionUtils.INSTANCE.packageHasCastingCapabilities(
                        packageManager, mPackageName);

        // Using application context for the dialog, instead of the activity context, so we get
        // the correct screen width when in split screen.
        Context dialogContext = getApplicationContext();
        final boolean overrideDisableSingleAppOption =
                CompatChanges.isChangeEnabled(
                        OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
                        mPackageName, getHostUserHandle());
        MediaProjectionPermissionDialogDelegate delegate =
                new MediaProjectionPermissionDialogDelegate(
                        dialogContext,
                        getMediaProjectionConfig(),
                        dialog -> {
                            ScreenShareOption selectedOption =
                                    dialog.getSelectedScreenShareOption();
                            grantMediaProjectionPermission(selectedOption.getMode());
                        },
                        () -> finish(RECORD_CANCEL, /* projection= */ null),
                        hasCastingCapabilities,
                        appName,
                        overrideDisableSingleAppOption,
                        mUid,
                        mMediaProjectionMetricsLogger);
        BaseMediaProjectionPermissionDialogDelegate<AlertDialog> delegate =
                createPermissionDialogDelegate(appName, hasCastingCapabilities, dialogContext);
        mDialog =
                new AlertDialogWithDelegate(
                        dialogContext, R.style.Theme_SystemUI_Dialog, delegate);
@@ -274,6 +257,44 @@ public class MediaProjectionPermissionActivity extends Activity
        return appName;
    }

    private BaseMediaProjectionPermissionDialogDelegate<AlertDialog> createPermissionDialogDelegate(
            String appName,
            boolean hasCastingCapabilities,
            Context dialogContext) {
        final boolean overrideDisableSingleAppOption =
                CompatChanges.isChangeEnabled(
                        OVERRIDE_DISABLE_MEDIA_PROJECTION_SINGLE_APP_OPTION,
                        mPackageName, getHostUserHandle());
        MediaProjectionConfig mediaProjectionConfig = getMediaProjectionConfig();
        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>> onStartRecordingClicked =
                dialog -> {
                    ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption();
                    grantMediaProjectionPermission(selectedOption.getMode());
                };
        Runnable onCancelClicked = () -> finish(RECORD_CANCEL, /* projection= */ null);
        if (hasCastingCapabilities) {
            return new SystemCastPermissionDialogDelegate(
                    dialogContext,
                    mediaProjectionConfig,
                    onStartRecordingClicked,
                    onCancelClicked,
                    appName,
                    overrideDisableSingleAppOption,
                    mUid,
                    mMediaProjectionMetricsLogger);
        } else {
            return new ShareToAppPermissionDialogDelegate(
                    dialogContext,
                    mediaProjectionConfig,
                    onStartRecordingClicked,
                    onCancelClicked,
                    appName,
                    overrideDisableSingleAppOption,
                    mUid,
                    mMediaProjectionMetricsLogger);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
+47 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.mediaprojection.permission

import android.content.Context
import android.media.projection.MediaProjectionConfig
import com.android.systemui.res.R

/** Various utility methods related to media projection permissions. */
object MediaProjectionPermissionUtils {
    fun getSingleAppDisabledText(
        context: Context,
        appName: String,
        mediaProjectionConfig: MediaProjectionConfig?,
        overrideDisableSingleAppOption: Boolean,
    ): String? {
        // The single app option should only be disabled if the client has setup a
        // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
        // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
        val singleAppOptionDisabled =
            !overrideDisableSingleAppOption &&
                mediaProjectionConfig?.regionToCapture ==
                    MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
        return if (singleAppOptionDisabled) {
            context.getString(
                R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
                appName,
            )
        } else {
            null
        }
    }
}
+25 −48
Original line number Diff line number Diff line
@@ -23,13 +23,16 @@ import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.res.R
import java.util.function.Consumer

/** Dialog to select screen recording options */
class MediaProjectionPermissionDialogDelegate(
/**
 * Dialog to select screen recording options for sharing the screen to another app on the same
 * device.
 */
class ShareToAppPermissionDialogDelegate(
    context: Context,
    mediaProjectionConfig: MediaProjectionConfig?,
    private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
    private val onStartRecordingClicked:
        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>>,
    private val onCancelClicked: Runnable,
    private val hasCastingCapabilities: Boolean,
    appName: String,
    forceShowPartialScreenshare: Boolean,
    hostUid: Int,
@@ -39,24 +42,18 @@ class MediaProjectionPermissionDialogDelegate(
        createOptionList(
            context,
            appName,
            hasCastingCapabilities,
            mediaProjectionConfig,
            forceShowPartialScreenshare
            overrideDisableSingleAppOption = forceShowPartialScreenshare,
        ),
        appName,
        hostUid,
        mediaProjectionMetricsLogger
        mediaProjectionMetricsLogger,
    ) {
    override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
        super.onCreate(dialog, savedInstanceState)
        // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
        if (hasCastingCapabilities) {
            setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
            setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
        } else {
        setDialogTitle(R.string.media_projection_entry_app_permission_dialog_title)
        setStartButtonText(R.string.media_projection_entry_app_permission_dialog_continue)
        }
        setStartButtonOnClickListener {
            // Note that it is important to run this callback before dismissing, so that the
            // callback can disable the dialog exit animation if it wants to.
@@ -73,55 +70,35 @@ class MediaProjectionPermissionDialogDelegate(
        private fun createOptionList(
            context: Context,
            appName: String,
            hasCastingCapabilities: Boolean,
            mediaProjectionConfig: MediaProjectionConfig?,
            overrideDisableSingleAppOption: Boolean = false,
            overrideDisableSingleAppOption: Boolean,
        ): List<ScreenShareOption> {
            val singleAppWarningText =
                if (hasCastingCapabilities) {
                    R.string.media_projection_entry_cast_permission_dialog_warning_single_app
                } else {
                    R.string.media_projection_entry_app_permission_dialog_warning_single_app
                }
            val entireScreenWarningText =
                if (hasCastingCapabilities) {
                    R.string.media_projection_entry_cast_permission_dialog_warning_entire_screen
                } else {
                    R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
                }

            // The single app option should only be disabled if the client has setup a
            // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND
            // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override.
            val singleAppOptionDisabled =
                !overrideDisableSingleAppOption &&
                    mediaProjectionConfig?.regionToCapture ==
                        MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY

            val singleAppDisabledText =
                if (singleAppOptionDisabled) {
                    context.getString(
                        R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
                        appName
                MediaProjectionPermissionUtils.getSingleAppDisabledText(
                    context,
                    appName,
                    mediaProjectionConfig,
                    overrideDisableSingleAppOption,
                )
                } else {
                    null
                }
            val options =
                listOf(
                    ScreenShareOption(
                        mode = SINGLE_APP,
                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
                        warningText = singleAppWarningText,
                        warningText =
                            R.string
                                .media_projection_entry_app_permission_dialog_warning_single_app,
                        spinnerDisabledText = singleAppDisabledText,
                    ),
                    ScreenShareOption(
                        mode = ENTIRE_SCREEN,
                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
                        warningText = entireScreenWarningText
                        warningText =
                            R.string
                                .media_projection_entry_app_permission_dialog_warning_entire_screen,
                    )
                )
            return if (singleAppOptionDisabled) {
            return if (singleAppDisabledText != null) {
                // Make sure "Entire screen" is the first option when "Single App" is disabled.
                options.reversed()
            } else {
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.mediaprojection.permission

import android.app.AlertDialog
import android.content.Context
import android.media.projection.MediaProjectionConfig
import android.os.Bundle
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.permission.MediaProjectionPermissionUtils.getSingleAppDisabledText
import com.android.systemui.res.R
import java.util.function.Consumer

/** Dialog to select screen recording options for casting the screen to a different device. */
class SystemCastPermissionDialogDelegate(
    context: Context,
    mediaProjectionConfig: MediaProjectionConfig?,
    private val onStartRecordingClicked:
        Consumer<BaseMediaProjectionPermissionDialogDelegate<AlertDialog>>,
    private val onCancelClicked: Runnable,
    appName: String,
    forceShowPartialScreenshare: Boolean,
    hostUid: Int,
    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
) :
    BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
        createOptionList(
            context,
            appName,
            mediaProjectionConfig,
            overrideDisableSingleAppOption = forceShowPartialScreenshare,
        ),
        appName,
        hostUid,
        mediaProjectionMetricsLogger,
    ) {
    override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
        super.onCreate(dialog, savedInstanceState)
        // TODO(b/270018943): Handle the case of System sharing (not recording nor casting)
        setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
        setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
        setStartButtonOnClickListener {
            // Note that it is important to run this callback before dismissing, so that the
            // callback can disable the dialog exit animation if it wants to.
            onStartRecordingClicked.accept(this)
            dialog.dismiss()
        }
        setCancelButtonOnClickListener {
            onCancelClicked.run()
            dialog.dismiss()
        }
    }

    companion object {
        private fun createOptionList(
            context: Context,
            appName: String,
            mediaProjectionConfig: MediaProjectionConfig?,
            overrideDisableSingleAppOption: Boolean,
        ): List<ScreenShareOption> {
            val singleAppDisabledText =
                getSingleAppDisabledText(
                    context,
                    appName,
                    mediaProjectionConfig,
                    overrideDisableSingleAppOption
                )
            val options =
                listOf(
                    ScreenShareOption(
                        mode = SINGLE_APP,
                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
                        warningText =
                            R.string
                                .media_projection_entry_cast_permission_dialog_warning_single_app,
                        spinnerDisabledText = singleAppDisabledText,
                    ),
                    ScreenShareOption(
                        mode = ENTIRE_SCREEN,
                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
                        warningText =
                            R.string
                                .media_projection_entry_cast_permission_dialog_warning_entire_screen,
                    )
                )
            return if (singleAppDisabledText != null) {
                // Make sure "Entire screen" is the first option when "Single App" is disabled.
                options.reversed()
            } else {
                options
            }
        }
    }
}
Loading