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

Commit cbfecec3 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Create dialogs that handles changing magnification mode" into main

parents 2fd74269 23600767
Loading
Loading
Loading
Loading
+186 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.accessibility.detail.screenmagnification.dialogs

import android.app.Dialog
import android.app.settings.SettingsEnums
import android.content.DialogInterface
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.LayoutInflater
import android.widget.AdapterView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import com.android.settings.R
import com.android.settings.accessibility.AccessibilityDialogUtils
import com.android.settings.accessibility.AccessibilityUtil
import com.android.settings.accessibility.ItemInfoArrayAdapter
import com.android.settings.accessibility.MagnificationCapabilities
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode
import com.android.settings.core.instrumentation.InstrumentedDialogFragment

/** Displays magnification mode options in a dialog */
class MagnificationModeChooser : InstrumentedDialogFragment() {
    private lateinit var requestKey: String
    private lateinit var modeInfos: List<MagnificationModeInfo>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestKey = requireArguments().getString(ARG_REQUEST_KEY, "")
        modeInfos =
            listOf(
                MagnificationModeInfo(
                    title =
                        getText(
                            R.string.accessibility_magnification_mode_dialog_option_full_screen
                        ),
                    summary = null,
                    drawableId = R.drawable.accessibility_magnification_mode_fullscreen,
                    mode = MagnificationMode.FULLSCREEN,
                ),
                MagnificationModeInfo(
                    title = getText(R.string.accessibility_magnification_mode_dialog_option_window),
                    summary = null,
                    drawableId = R.drawable.accessibility_magnification_mode_window,
                    mode = MagnificationMode.WINDOW,
                ),
                MagnificationModeInfo(
                    title = getText(R.string.accessibility_magnification_mode_dialog_option_switch),
                    summary =
                        getText(
                            R.string.accessibility_magnification_area_settings_mode_switch_summary
                        ),
                    drawableId = R.drawable.accessibility_magnification_mode_switch,
                    mode = MagnificationMode.ALL,
                ),
            )
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listView =
            AccessibilityDialogUtils.createSingleChoiceListView(
                requireContext(),
                modeInfos,
                /* itemListener= */ null,
            )

        val headerView =
            LayoutInflater.from(requireContext())
                .inflate(R.layout.accessibility_dialog_header, listView, /* attachToRoot= */ false)

        headerView
            .requireViewById<TextView>(R.id.accessibility_dialog_header_text_view)
            .setText(R.string.accessibility_magnification_area_settings_message)
        listView.addHeaderView(headerView, /* data= */ null, /* isSelectable= */ false)

        if (savedInstanceState == null) {
            // Sets up initial selected item
            val selectedMode = MagnificationCapabilities.getCapabilities(requireContext())
            val selectedIndex =
                listView.adapter.run {
                    for (index in 0 until count) {
                        val modeInfo = getItem(index) as? MagnificationModeInfo
                        if (modeInfo?.mode == selectedMode) {
                            return@run index
                        }
                    }
                    AdapterView.INVALID_POSITION
                }
            if (selectedIndex != AdapterView.INVALID_POSITION) {
                listView.setItemChecked(selectedIndex, true)
            }
        }

        return AccessibilityDialogUtils.createCustomDialog(
            requireContext(),
            getText(R.string.accessibility_magnification_mode_dialog_title),
            listView,
            getText(R.string.save),
            DialogInterface.OnClickListener { _, _ ->
                val selectedModeInfo: MagnificationModeInfo? =
                    listView.checkedItemPosition.let {
                        if (it == AdapterView.INVALID_POSITION) {
                            null
                        } else {
                            listView.adapter.getItem(it) as? MagnificationModeInfo
                        }
                    }

                confirmSelection(selectedModeInfo)
            },
            getText(R.string.cancel),
            /* negativeListener= */ null,
        )
    }

    private fun confirmSelection(modeInfo: MagnificationModeInfo?) {
        if (modeInfo == null) {
            Log.w(TAG, "Selected positive button with INVALID_POSITION index")
            return
        }
        val mode = modeInfo.mode

        val isTripleTapEnabled =
            Settings.Secure.getInt(
                requireContext().contentResolver,
                Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
                AccessibilityUtil.State.OFF,
            ) == AccessibilityUtil.State.ON

        if (isTripleTapEnabled && mode != MagnificationMode.FULLSCREEN) {
            TripleTapWarningDialog.showDialog(parentFragmentManager, requestKey, mode)
        } else {
            MagnificationCapabilities.setCapabilities(requireContext(), mode)
            setFragmentResult(requestKey, Bundle().apply { putInt(RESULT, mode) })
        }
    }

    override fun getMetricsCategory(): Int {
        return SettingsEnums.DIALOG_MAGNIFICATION_CAPABILITY
    }

    companion object {
        private const val TAG = "MagnificationModeChooser"
        internal const val ARG_REQUEST_KEY = "requestKey"
        internal const val RESULT = "selectedMode"

        @JvmStatic
        fun showDialog(fragmentManager: FragmentManager, requestKey: String) {
            val bundle = Bundle().apply { putString(ARG_REQUEST_KEY, requestKey) }
            MagnificationModeChooser().apply {
                arguments = bundle
                show(fragmentManager, /* tag= */ MagnificationModeChooser::class.simpleName)
            }
        }

        @JvmStatic
        @MagnificationMode
        fun getCheckedModeFromResult(bundle: Bundle): Int {
            return bundle.getInt(RESULT)
        }
    }
}

class MagnificationModeInfo(
    title: CharSequence,
    summary: CharSequence?,
    @DrawableRes drawableId: Int,
    @MagnificationMode val mode: Int,
) : ItemInfoArrayAdapter.ItemInfo(title, summary, drawableId)
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.accessibility.detail.screenmagnification.dialogs

import android.app.Dialog
import android.app.settings.SettingsEnums
import android.content.DialogInterface
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.settings.R
import com.android.settings.accessibility.AccessibilityDialogUtils
import com.android.settings.accessibility.MagnificationCapabilities
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode
import com.android.settings.accessibility.detail.screenmagnification.dialogs.MagnificationModeChooser.Companion.ARG_REQUEST_KEY
import com.android.settings.accessibility.detail.screenmagnification.dialogs.MagnificationModeChooser.Companion.RESULT
import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
import com.android.settings.utils.AnnotationSpan

/** Display performance warning when using triple tap with the selected magnification mode. */
class TripleTapWarningDialog : InstrumentedDialogFragment() {
    private lateinit var requestKey: String
    @MagnificationMode private var selectedMode = MagnificationMode.FULLSCREEN

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        requestKey = requireArguments().getString(ARG_REQUEST_KEY, "")
        selectedMode = requireArguments().getInt(ARG_SELECTED_MODE)
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val contentView: View =
            LayoutInflater.from(requireContext())
                .inflate(R.layout.magnification_triple_tap_warning_dialog, /* root= */ null)
                .also { updateLink(it) }

        val title = getText(R.string.accessibility_magnification_triple_tap_warning_title)
        val positiveBtnText =
            getText(R.string.accessibility_magnification_triple_tap_warning_positive_button)
        val negativeBtnText =
            getText(R.string.accessibility_magnification_triple_tap_warning_negative_button)

        val dialog =
            AccessibilityDialogUtils.createCustomDialog(
                requireContext(),
                title,
                contentView,
                positiveBtnText,
                DialogInterface.OnClickListener { _, _ -> confirmSelection() },
                negativeBtnText,
                DialogInterface.OnClickListener { _, _ ->
                    MagnificationModeChooser.showDialog(parentFragmentManager, requestKey)
                },
            )

        return dialog
    }

    private fun updateLink(view: View) {
        val messageView = view.requireViewById<TextView>(R.id.message)

        val linkListener =
            View.OnClickListener { _ ->
                confirmSelection()
                showEditShortcutsScreen()
                dismiss()
            }
        val linkInfo =
            AnnotationSpan.LinkInfo(AnnotationSpan.LinkInfo.DEFAULT_ANNOTATION, linkListener)
        val textWithLink =
            AnnotationSpan.linkify(
                getText(R.string.accessibility_magnification_triple_tap_warning_message),
                linkInfo,
            )

        messageView.apply {
            text = textWithLink
            movementMethod = LinkMovementMethod.getInstance()
        }
    }

    private fun confirmSelection() {
        MagnificationCapabilities.setCapabilities(requireContext(), selectedMode)
        setFragmentResult(requestKey, Bundle().apply { putInt(RESULT, selectedMode) })
    }

    private fun showEditShortcutsScreen() {
        EditShortcutsPreferenceFragment.showEditShortcutScreen(
            requireContext(),
            metricsCategory,
            getText(R.string.accessibility_screen_magnification_shortcut_title),
            AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME,
            activity?.intent,
        )
    }

    override fun getMetricsCategory(): Int {
        return SettingsEnums.DIALOG_MAGNIFICATION_TRIPLE_TAP_WARNING
    }

    companion object {
        private const val ARG_SELECTED_MODE = "selectedMode"

        @JvmStatic
        fun showDialog(
            fragmentManager: FragmentManager,
            requestKey: String,
            @MagnificationMode selectedMagnificationMode: Int,
        ) {
            val bundle =
                Bundle().apply {
                    putString(ARG_REQUEST_KEY, requestKey)
                    putInt(ARG_SELECTED_MODE, selectedMagnificationMode)
                }

            TripleTapWarningDialog().apply {
                arguments = bundle
                show(fragmentManager, /* tag= */ MagnificationModeChooser::class.simpleName)
            }
        }
    }
}
+358 −0

File added.

Preview size limit exceeded, changes collapsed.

+194 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.settings.accessibility.detail.screenmagnification.dialogs

import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.text.SpannableString
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentResultListener
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragment
import androidx.test.core.app.ApplicationProvider
import com.android.settings.R
import com.android.settings.accessibility.MagnificationCapabilities
import com.android.settings.accessibility.MagnificationCapabilities.MagnificationMode
import com.android.settings.accessibility.detail.screenmagnification.dialogs.MagnificationModeChooser.Companion.getCheckedModeFromResult
import com.android.settings.testutils.AccessibilityTestUtils
import com.android.settings.utils.AnnotationSpan
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowDialog
import org.robolectric.shadows.ShadowLooper

/** Tests for [TripleTapWarningDialog] */
@RunWith(RobolectricTestRunner::class)
class TripleTapWarningDialogTest {
    @get:Rule val mockito = MockitoJUnit.rule()
    private val requestKey = "requestFromTest"
    private val initialModeInSetting = MagnificationMode.FULLSCREEN
    private val selectedMode = MagnificationMode.WINDOW
    private val context: Context = ApplicationProvider.getApplicationContext()
    private lateinit var fragmentScenario: FragmentScenario<Fragment>
    private lateinit var fragment: Fragment
    @Mock private lateinit var mockFragResultListener: FragmentResultListener
    @Captor lateinit var responseCaptor: ArgumentCaptor<Bundle>

    @Before
    fun setUp() {
        MagnificationCapabilities.setCapabilities(context, initialModeInSetting)
        fragmentScenario = launchFragment(themeResId = androidx.appcompat.R.style.Theme_AppCompat)
        fragmentScenario.onFragment { frag ->
            fragment = frag
            fragment.childFragmentManager.setFragmentResultListener(
                requestKey,
                fragment,
                mockFragResultListener,
            )
        }
    }

    @After
    fun cleanUp() {
        fragmentScenario.close()
    }

    @Test
    fun launchDialog_verifyTitleText() {
        val alertDialog = shadowOf(launchDialog())
        assertThat(alertDialog.title.toString())
            .isEqualTo(
                context.getString(R.string.accessibility_magnification_triple_tap_warning_title)
            )
    }

    @Test
    fun launchDialog_verifyButtonsText() {
        val alertDialog = launchDialog() as AlertDialog
        val positiveBtn = alertDialog.getButton(DialogInterface.BUTTON_POSITIVE)
        val negativeBtn = alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE)

        assertThat(positiveBtn.text.toString())
            .isEqualTo(
                context.getString(
                    R.string.accessibility_magnification_triple_tap_warning_positive_button
                )
            )
        assertThat(negativeBtn.text.toString())
            .isEqualTo(
                context.getString(
                    R.string.accessibility_magnification_triple_tap_warning_negative_button
                )
            )
    }

    @Test
    fun launchDialog_verifyContentText() {
        val alertDialog = launchDialog() as AlertDialog
        val messageView: TextView = alertDialog.requireViewById(R.id.message)
        assertThat(messageView.text.toString())
            .isEqualTo(
                context.getString(R.string.accessibility_magnification_triple_tap_warning_message)
            )
    }

    @Test
    fun clickPositiveButton_saveSelectedModeSetting() {
        val alertDialog = launchDialog() as AlertDialog

        alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick()
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

        assertThat(MagnificationCapabilities.getCapabilities(context)).isEqualTo(selectedMode)
        verify(mockFragResultListener).onFragmentResult(eq(requestKey), responseCaptor.capture())
        val response = responseCaptor.value
        assertThat(getCheckedModeFromResult(response)).isEqualTo(selectedMode)
    }

    @Test
    fun clickNegativeButton_selectedModeSettingUnchanged() {
        val alertDialog = launchDialog() as AlertDialog

        alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

        assertThat(MagnificationCapabilities.getCapabilities(context))
            .isEqualTo(initialModeInSetting)
        verify(mockFragResultListener, never()).onFragmentResult(eq(requestKey), any())
    }

    @Test
    fun clickNegativeButton_showMagnificationModeChooser() {
        val alertDialog = launchDialog() as AlertDialog

        alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).performClick()
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

        val modeDialogTitle =
            context.getString(R.string.accessibility_magnification_mode_dialog_title)
        val dialog = ShadowDialog.getLatestDialog()
        assertThat(dialog).isNotNull()
        assertThat(shadowOf(dialog).title).isEqualTo(modeDialogTitle)
    }

    @Test
    fun clickChangeShortcuts_showEditShortcutScreen() {
        val alertDialog = launchDialog() as AlertDialog
        val messageView: TextView = alertDialog.requireViewById(R.id.message)
        val message = messageView.text
        assertThat(message).isInstanceOf(SpannableString::class.java)
        val spans =
            (message as SpannableString).getSpans<AnnotationSpan>(
                0,
                message.length,
                AnnotationSpan::class.java,
            )

        spans[0].onClick(messageView)

        AccessibilityTestUtils.assertEditShortcutsScreenShown(fragment)
    }

    private fun launchDialog(): Dialog {
        TripleTapWarningDialog.showDialog(
            fragmentManager = fragment.childFragmentManager,
            requestKey = requestKey,
            selectedMagnificationMode = selectedMode,
        )
        ShadowLooper.runUiThreadTasksIncludingDelayedTasks()

        return ShadowDialog.getLatestDialog()
    }
}