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

Commit ff3b69cd authored by dakinola's avatar dakinola Committed by Daniel Akinola
Browse files

Update string logic for disabling MediaProjection single app option

Previously, the single app option for MediaProjection could only be disabled if there was an app name provided when building the dialog. Additionally, we only attempted to extract an app name if the requesting package didn't have cast permission. Now we always attempt to extract an app name (using the package name as a fallback), remove the condition of there being an app name to disabling the single app option and no longer rely on the app name being null to indicate if a package has cast permissions.

Bug: 329362311
Bug: 348323406
Flag: EXEMPT bugfix
Test: atest MediaProjectionPermissionDialogDelegateTest
Change-Id: Ica761c7145856ef7e701cc1fc6ef4c5e72938915
parent 983f5c5e
Loading
Loading
Loading
Loading
+48 −33
Original line number Diff line number Diff line
@@ -187,44 +187,17 @@ public class MediaProjectionPermissionActivity extends Activity
            }
        }

        TextPaint paint = new TextPaint();
        paint.setTextSize(42);

        CharSequence dialogText = null;
        CharSequence dialogTitle = null;
        String appName = null;
        if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) {

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

        if (hasCastingCapabilities) {
            dialogText = getString(R.string.media_projection_sys_service_dialog_warning);
            dialogTitle = getString(R.string.media_projection_sys_service_dialog_title);
        } else {
            String label = aInfo.loadLabel(packageManager).toString();

            // If the label contains new line characters it may push the security
            // message below the fold of the dialog. Labels shouldn't have new line
            // characters anyways, so just truncate the message the first time one
            // is seen.
            final int labelLength = label.length();
            int offset = 0;
            while (offset < labelLength) {
                final int codePoint = label.codePointAt(offset);
                final int type = Character.getType(codePoint);
                if (type == Character.LINE_SEPARATOR
                        || type == Character.CONTROL
                        || type == Character.PARAGRAPH_SEPARATOR) {
                    label = label.substring(0, offset) + ELLIPSIS;
                    break;
                }
                offset += Character.charCount(codePoint);
            }

            if (label.isEmpty()) {
                label = mPackageName;
            }

            String unsanitizedAppName = TextUtils.ellipsize(label,
                    paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();
            appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);

            String actionText = getString(R.string.media_projection_dialog_warning, appName);
            SpannableString message = new SpannableString(actionText);

@@ -255,6 +228,7 @@ public class MediaProjectionPermissionActivity extends Activity
                                grantMediaProjectionPermission(selectedOption.getMode());
                            },
                            () -> finish(RECORD_CANCEL, /* projection= */ null),
                            hasCastingCapabilities,
                            appName,
                            overrideDisableSingleAppOption,
                            mUid,
@@ -289,6 +263,47 @@ public class MediaProjectionPermissionActivity extends Activity
        }
    }

    private String extractAppName(ApplicationInfo applicationInfo, PackageManager packageManager) {
        String label = applicationInfo.loadLabel(packageManager).toString();

        // If the label contains new line characters it may push the security
        // message below the fold of the dialog. Labels shouldn't have new line
        // characters anyways, so just truncate the message the first time one
        // is seen.
        final int labelLength = label.length();
        int offset = 0;
        while (offset < labelLength) {
            final int codePoint = label.codePointAt(offset);
            final int type = Character.getType(codePoint);
            if (type == Character.LINE_SEPARATOR
                    || type == Character.CONTROL
                    || type == Character.PARAGRAPH_SEPARATOR) {
                label = label.substring(0, offset) + ELLIPSIS;
                break;
            }
            offset += Character.charCount(codePoint);
        }

        if (label.isEmpty()) {
            label = mPackageName;
        }

        TextPaint paint = new TextPaint();
        paint.setTextSize(42);

        String unsanitizedAppName = TextUtils.ellipsize(label,
                paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString();
        String appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName);

        // Have app name be the package name as a default fallback, if specific app name can't be
        // extracted
        if (appName == null || appName.isEmpty()) {
            return mPackageName;
        }

        return appName;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
+18 −12
Original line number Diff line number Diff line
@@ -29,13 +29,20 @@ class MediaProjectionPermissionDialogDelegate(
    mediaProjectionConfig: MediaProjectionConfig?,
    private val onStartRecordingClicked: Consumer<MediaProjectionPermissionDialogDelegate>,
    private val onCancelClicked: Runnable,
    private val appName: String?,
    private val hasCastingCapabilities: Boolean,
    appName: String,
    forceShowPartialScreenshare: Boolean,
    hostUid: Int,
    mediaProjectionMetricsLogger: MediaProjectionMetricsLogger,
) :
    BaseMediaProjectionPermissionDialogDelegate<AlertDialog>(
        createOptionList(context, appName, mediaProjectionConfig, forceShowPartialScreenshare),
        createOptionList(
            context,
            appName,
            hasCastingCapabilities,
            mediaProjectionConfig,
            forceShowPartialScreenshare
        ),
        appName,
        hostUid,
        mediaProjectionMetricsLogger
@@ -43,7 +50,7 @@ class MediaProjectionPermissionDialogDelegate(
    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 (appName == null) {
        if (hasCastingCapabilities) {
            setDialogTitle(R.string.media_projection_entry_cast_permission_dialog_title)
            setStartButtonText(R.string.media_projection_entry_cast_permission_dialog_continue)
        } else {
@@ -65,29 +72,28 @@ class MediaProjectionPermissionDialogDelegate(
    companion object {
        private fun createOptionList(
            context: Context,
            appName: String?,
            appName: String,
            hasCastingCapabilities: Boolean,
            mediaProjectionConfig: MediaProjectionConfig?,
            overrideDisableSingleAppOption: Boolean = false,
        ): List<ScreenShareOption> {
            val singleAppWarningText =
                if (appName == null) {
                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 (appName == null) {
                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 there is an app name provided,
            // 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.
            // 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 =
                appName != null &&
                !overrideDisableSingleAppOption &&
                    mediaProjectionConfig?.regionToCapture ==
                        MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+60 −23
Original line number Diff line number Diff line
@@ -47,13 +47,7 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
    private lateinit var dialog: AlertDialog

    private val flags = mock<FeatureFlagsClassic>()
    private val onStartRecordingClicked = mock<Runnable>()
    private val mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()

    private val mediaProjectionConfig: MediaProjectionConfig =
        MediaProjectionConfig.createConfigForDefaultDisplay()
    private val appName: String = "testApp"
    private val hostUid: Int = 12345
    private val appName = "Test App"

    private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
    private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
@@ -73,14 +67,31 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
    }

    @Test
    fun showDialog_forceShowPartialScreenShareFalse() {
        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
        // overrideDisableSingleAppOption = false
        val overrideDisableSingleAppOption = false
        setUpAndShowDialog(overrideDisableSingleAppOption)
    fun showDefaultDialog() {
        setUpAndShowDialog()

        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
        val secondOptionText =
            spinner.adapter
                .getDropDownView(1, null, spinner)
                .findViewById<TextView>(android.R.id.text1)
                ?.text

        // check that the first option is single app and enabled
        assertEquals(context.getString(resIdSingleApp), spinner.selectedItem)

        // check that the second option is full screen and enabled
        assertEquals(context.getString(resIdFullScreen), secondOptionText)
    }

    @Test
    fun showDialog_disableSingleApp() {
        setUpAndShowDialog(
            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay()
        )

        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
        val secondOptionWarningText =
            spinner.adapter
                .getDropDownView(1, null, spinner)
                .findViewById<TextView>(android.R.id.text2)
@@ -90,15 +101,15 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)

        // check that the second option is single app and disabled
        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionText)
        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
    }

    @Test
    fun showDialog_forceShowPartialScreenShareTrue() {
        // Set up dialog with MediaProjectionConfig.createConfigForDefaultDisplay() and
        // overrideDisableSingleAppOption = true
        val overrideDisableSingleAppOption = true
        setUpAndShowDialog(overrideDisableSingleAppOption)
    fun showDialog_disableSingleApp_forceShowPartialScreenShareTrue() {
        setUpAndShowDialog(
            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
            overrideDisableSingleAppOption = true
        )

        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
        val secondOptionText =
@@ -114,17 +125,43 @@ class MediaProjectionPermissionDialogDelegateTest : SysuiTestCase() {
        assertEquals(context.getString(resIdFullScreen), secondOptionText)
    }

    private fun setUpAndShowDialog(overrideDisableSingleAppOption: Boolean) {
    @Test
    fun showDialog_disableSingleApp_hasCastingCapabilities() {
        setUpAndShowDialog(
            mediaProjectionConfig = MediaProjectionConfig.createConfigForDefaultDisplay(),
            hasCastingCapabilities = true
        )

        val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
        val secondOptionWarningText =
            spinner.adapter
                .getDropDownView(1, null, spinner)
                .findViewById<TextView>(android.R.id.text2)
                ?.text

        // check that the first option is full screen and enabled
        assertEquals(context.getString(resIdFullScreen), spinner.selectedItem)

        // check that the second option is single app and disabled
        assertEquals(context.getString(resIdSingleAppDisabled, appName), secondOptionWarningText)
    }

    private fun setUpAndShowDialog(
        mediaProjectionConfig: MediaProjectionConfig? = null,
        overrideDisableSingleAppOption: Boolean = false,
        hasCastingCapabilities: Boolean = false,
    ) {
        val delegate =
            MediaProjectionPermissionDialogDelegate(
                context,
                mediaProjectionConfig,
                {},
                onStartRecordingClicked,
                onStartRecordingClicked = {},
                onCancelClicked = {},
                hasCastingCapabilities,
                appName,
                overrideDisableSingleAppOption,
                hostUid,
                mediaProjectionMetricsLogger
                hostUid = 12345,
                mediaProjectionMetricsLogger = mock<MediaProjectionMetricsLogger>()
            )

        dialog = AlertDialogWithDelegate(context, R.style.Theme_SystemUI_Dialog, delegate)