[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-152.0a1-16.0-2] 2 commits: fixup! [android] Implement Android-native Connection Assist UI



Title: GitLab

Dan Ballard pushed to branch tor-browser-152.0a1-16.0-2 at The Tor Project / Applications / Tor Browser

Commits:

  • 5eb5f5ef
    by clairehurst at 2026-06-08T15:31:58-07:00
    fixup! [android] Implement Android-native Connection Assist UI
    
    Bug 43576: Connection Assist on Android Fast Follows (Bug 41188)
    Add frequent regions UI
    
  • 21e669d3
    by clairehurst at 2026-06-08T15:31:58-07:00
    [android] TBA strings
    
    Bug 43576: Connection Assist on Android Fast Follows (Bug 41188)
    Add frequent regions UI
    

5 changed files:

Changes:

  • mobile/android/android-components/components/ui/icons/src/main/res/drawable/unthemed_dropdown_arrow.xml
    1
    +<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
    
    2
    +      
    
    3
    +    <path android:fillColor="#FBFBFE" android:pathData="M7,10L12,15L17,10H7Z"/>
    
    4
    +    
    
    5
    +</vector>

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistFragment.kt
    ... ... @@ -4,7 +4,6 @@
    4 4
     
    
    5 5
     package org.mozilla.fenix.tor
    
    6 6
     
    
    7
    -import android.content.Intent
    
    8 7
     import android.graphics.Color
    
    9 8
     import android.graphics.Typeface
    
    10 9
     import android.os.Build
    
    ... ... @@ -18,9 +17,40 @@ import android.util.Log
    18 17
     import android.view.LayoutInflater
    
    19 18
     import android.view.View
    
    20 19
     import android.view.ViewGroup
    
    21
    -import android.widget.AdapterView
    
    22
    -import android.widget.ArrayAdapter
    
    23 20
     import androidx.appcompat.content.res.AppCompatResources
    
    21
    +import androidx.compose.foundation.Image
    
    22
    +import androidx.compose.foundation.background
    
    23
    +import androidx.compose.foundation.layout.Box
    
    24
    +import androidx.compose.foundation.layout.Column
    
    25
    +import androidx.compose.foundation.layout.Row
    
    26
    +import androidx.compose.foundation.layout.Spacer
    
    27
    +import androidx.compose.foundation.layout.fillMaxWidth
    
    28
    +import androidx.compose.foundation.layout.height
    
    29
    +import androidx.compose.foundation.layout.padding
    
    30
    +import androidx.compose.foundation.layout.width
    
    31
    +import androidx.compose.foundation.lazy.LazyColumn
    
    32
    +import androidx.compose.material3.DividerDefaults
    
    33
    +import androidx.compose.material3.DropdownMenu
    
    34
    +import androidx.compose.material3.DropdownMenuItem
    
    35
    +import androidx.compose.material3.HorizontalDivider
    
    36
    +import androidx.compose.material3.Text
    
    37
    +import androidx.compose.material3.TextButton
    
    38
    +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
    
    39
    +import androidx.compose.material3.adaptive.currentWindowDpSize
    
    40
    +import androidx.compose.runtime.Composable
    
    41
    +import androidx.compose.runtime.collectAsState
    
    42
    +import androidx.compose.runtime.getValue
    
    43
    +import androidx.compose.runtime.mutableStateOf
    
    44
    +import androidx.compose.runtime.saveable.rememberSaveable
    
    45
    +import androidx.compose.runtime.setValue
    
    46
    +import androidx.compose.ui.Modifier
    
    47
    +import androidx.compose.ui.platform.ViewCompositionStrategy
    
    48
    +import androidx.compose.ui.res.painterResource
    
    49
    +import androidx.compose.ui.text.TextStyle
    
    50
    +import androidx.compose.ui.text.font.FontWeight
    
    51
    +import androidx.compose.ui.unit.DpOffset
    
    52
    +import androidx.compose.ui.unit.dp
    
    53
    +import androidx.compose.ui.unit.sp
    
    24 54
     import androidx.core.view.isEmpty
    
    25 55
     import androidx.fragment.app.Fragment
    
    26 56
     import androidx.fragment.app.activityViewModels
    
    ... ... @@ -29,8 +59,10 @@ import androidx.lifecycle.Lifecycle
    29 59
     import androidx.lifecycle.lifecycleScope
    
    30 60
     import androidx.lifecycle.repeatOnLifecycle
    
    31 61
     import androidx.navigation.fragment.findNavController
    
    62
    +import kotlinx.coroutines.flow.MutableStateFlow
    
    32 63
     import kotlinx.coroutines.launch
    
    33 64
     import mozilla.components.support.base.feature.UserInteractionHandler
    
    65
    +import mozilla.components.ui.colors.PhotonColors
    
    34 66
     import mozilla.components.ui.colors.R as colorsR
    
    35 67
     import org.mozilla.fenix.HomeActivity
    
    36 68
     import org.mozilla.fenix.R
    
    ... ... @@ -48,7 +80,9 @@ class TorConnectionAssistFragment : Fragment(), UserInteractionHandler, SystemIn
    48 80
         private var _binding: FragmentTorConnectionAssistBinding? = null
    
    49 81
         private val binding get() = _binding!!
    
    50 82
     
    
    51
    -    private lateinit var regionDropDownSpinnerAdapter: ArrayAdapter<String>
    
    83
    +    private val firstMenuItem: MutableStateFlow<String> by lazy {
    
    84
    +        MutableStateFlow(getString(R.string.connection_assist_automatic_country_detection))
    
    85
    +    }
    
    52 86
     
    
    53 87
         override fun onCreateView(
    
    54 88
             inflater: LayoutInflater,
    
    ... ... @@ -210,95 +244,186 @@ class TorConnectionAssistFragment : Fragment(), UserInteractionHandler, SystemIn
    210 244
     
    
    211 245
         private fun updateRegionDropdown(screen: ConnectAssistUiState) {
    
    212 246
             if (screen.regionDropDownVisible) {
    
    213
    -            if (binding.countryDropDown.isEmpty()) {
    
    214
    -                regionDropDownSpinnerAdapter = initializeSpinner()
    
    247
    +            firstMenuItem.value = getString(screen.regionDropDownDefaultItem)
    
    248
    +            if (binding.regionDropDown.isEmpty()) {
    
    249
    +                binding.regionDropDown.apply {
    
    250
    +                    setContent {
    
    251
    +                        setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
    
    252
    +                        RegionDropDown()
    
    253
    +                    }
    
    254
    +                }
    
    215 255
                     torConnectionAssistViewModel.fetchFrequentRegions()
    
    216 256
                     torConnectionAssistViewModel.fetchRegionNames()
    
    217 257
                 }
    
    218 258
     
    
    219
    -            setFirstItemInCountryDropDown(getString(screen.regionDropDownDefaultItem))
    
    220
    -
    
    221 259
                 if (screen == ConnectAssistUiState.ChooseRegion || screen == ConnectAssistUiState.ConfirmRegion || screen == ConnectAssistUiState.RegionNotFound) {
    
    222 260
                     torConnectionAssistViewModel.selectDefaultRegion()
    
    223
    -                setDropDownSelectionToSelectedCountryCode()
    
    224 261
                 }
    
    225 262
     
    
    226
    -            binding.unblockTheInternetInCountryDescription.visibility = View.VISIBLE
    
    227
    -            binding.countryDropDown.visibility = View.VISIBLE
    
    263
    +            binding.regionDropDown.visibility = View.VISIBLE
    
    228 264
             } else {
    
    229
    -            binding.unblockTheInternetInCountryDescription.visibility = View.GONE
    
    230
    -            binding.countryDropDown.visibility = View.GONE
    
    265
    +            binding.regionDropDown.visibility = View.GONE
    
    231 266
             }
    
    232 267
         }
    
    233 268
     
    
    234
    -    private fun setDropDownSelectionToSelectedCountryCode() {
    
    235
    -        binding.countryDropDown.setSelection(
    
    236
    -            indexOfSelectedCountryCode(),
    
    237
    -        )
    
    238
    -    }
    
    239
    -
    
    240
    -    private fun indexOfSelectedCountryCode() : Int {
    
    241
    -        return torConnectionAssistViewModel.regionCodeNameMap.value?.keys?.indexOf(
    
    242
    -            torConnectionAssistViewModel.selectedCountryCode.value,
    
    243
    -        )?.plus(1) ?: 0
    
    244
    -    }
    
    245
    -
    
    246
    -    private fun setFirstItemInCountryDropDown(item: String) {
    
    247
    -        if (!regionDropDownSpinnerAdapter.isEmpty) {
    
    248
    -            regionDropDownSpinnerAdapter.remove(regionDropDownSpinnerAdapter.getItem(0))
    
    249
    -        }
    
    250
    -        regionDropDownSpinnerAdapter.insert(item, 0)
    
    251
    -    }
    
    252
    -
    
    253
    -    private fun initializeSpinner(): ArrayAdapter<String> {
    
    254
    -        val spinnerAdapter: ArrayAdapter<String> =
    
    255
    -            ArrayAdapter<String>(
    
    256
    -                requireContext(),
    
    257
    -                android.R.layout.simple_spinner_item,
    
    258
    -                android.R.id.text1,
    
    259
    -            )
    
    260
    -        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
    
    261
    -        binding.countryDropDown.adapter = spinnerAdapter
    
    262
    -        binding.countryDropDown.onItemSelectedListener =
    
    263
    -            object : AdapterView.OnItemSelectedListener {
    
    264
    -                override fun onItemSelected(
    
    265
    -                    parent: AdapterView<*>?,
    
    266
    -                    view: View?,
    
    267
    -                    position: Int,
    
    268
    -                    id: Long,
    
    269
    -                ) {
    
    270
    -                    torConnectionAssistViewModel.setCountryCodeToSelectedItem(position)
    
    271
    -                    updateButton1(torConnectionAssistViewModel.torConnectScreen.value)
    
    272
    -                }
    
    273
    -
    
    274
    -                override fun onNothingSelected(parent: AdapterView<*>?) {}
    
    275
    -            }
    
    276
    -
    
    277
    -        viewLifecycleOwner.lifecycleScope.launch {
    
    278
    -            repeatOnLifecycle(Lifecycle.State.STARTED) {
    
    279
    -                torConnectionAssistViewModel.regionCodeNameMap.collect {
    
    280
    -                    if (it != null) {
    
    281
    -                        spinnerAdapter.clear()
    
    282
    -                        spinnerAdapter.add(getString(torConnectionAssistViewModel.torConnectScreen.value.regionDropDownDefaultItem))
    
    283
    -                        spinnerAdapter.addAll(it.values)
    
    269
    +    @Composable
    
    270
    +    fun RegionDropDown(
    
    271
    +        textStyle: TextStyle = TextStyle(
    
    272
    +            fontSize = 16.sp,
    
    273
    +            lineHeight = 24.sp,
    
    274
    +            fontWeight = FontWeight(400),
    
    275
    +            color = PhotonColors.LightGrey05,
    
    276
    +            letterSpacing = 0.15.sp,
    
    277
    +        ),
    
    278
    +        labelTextStyle: TextStyle = TextStyle(
    
    279
    +            fontSize = 14.sp,
    
    280
    +            lineHeight = 20.sp,
    
    281
    +            fontWeight = FontWeight(400),
    
    282
    +            color = PhotonColors.LightGrey40,
    
    283
    +            letterSpacing = 0.25.sp,
    
    284
    +        ),
    
    285
    +    ) {
    
    286
    +        var expanded by rememberSaveable { mutableStateOf(false) }
    
    287
    +        Box(
    
    288
    +            modifier = Modifier.fillMaxWidth()
    
    289
    +        ) {
    
    290
    +            TextButton(
    
    291
    +                onClick = { expanded = !expanded },
    
    292
    +                modifier = Modifier
    
    293
    +                    .fillMaxWidth()
    
    294
    +                    .padding(horizontal = 16.dp),
    
    295
    +            ) {
    
    296
    +                Column {
    
    297
    +                    Text(
    
    298
    +                        getString(R.string.connection_assist_unblock_the_internet_in_country_or_region),
    
    299
    +                        color = PhotonColors.LightGrey05,
    
    300
    +                        modifier = Modifier.padding(bottom = 8.dp),
    
    301
    +                        fontSize = 14.sp,
    
    302
    +                    )
    
    303
    +                    Row {
    
    304
    +                        Text(
    
    305
    +                            torConnectionAssistViewModel.regionCodeNameMap.collectAsState().value?.get(
    
    306
    +                                torConnectionAssistViewModel.selectedCountryCode.collectAsState().value,
    
    307
    +                            ) ?: firstMenuItem.collectAsState().value,
    
    308
    +                            color = PhotonColors.LightGrey05,
    
    309
    +                            fontSize = 14.sp,
    
    310
    +                        )
    
    311
    +                        Spacer(modifier = Modifier.weight(1f))
    
    312
    +                        Image(
    
    313
    +                            painterResource(mozilla.components.ui.icons.R.drawable.unthemed_dropdown_arrow),
    
    314
    +                            contentDescription = null,
    
    315
    +                        )
    
    284 316
                         }
    
    317
    +                    HorizontalDivider(Modifier, thickness = 1.dp, color = PhotonColors.LightGrey05)
    
    285 318
                     }
    
    286 319
                 }
    
    287
    -        }
    
    288
    -
    
    289
    -        viewLifecycleOwner.lifecycleScope.launch {
    
    290
    -            repeatOnLifecycle(Lifecycle.State.STARTED) {
    
    291
    -                torConnectionAssistViewModel.frequentRegionCodes.collect {
    
    292
    -                    if (it != null) {
    
    293
    -                        for (region in it) {
    
    294
    -                            Log.d(TAG, "collected region: $region")
    
    320
    +            DropdownMenu(
    
    321
    +                expanded = expanded,
    
    322
    +                onDismissRequest = { expanded = false },
    
    323
    +                offset = DpOffset(x = 24.dp, y = 0.dp),
    
    324
    +                modifier = Modifier.background(color = PhotonColors.Violet90)
    
    325
    +            ) {
    
    326
    +                @OptIn(ExperimentalMaterial3AdaptiveApi::class)
    
    327
    +                LazyColumn(
    
    328
    +                        Modifier
    
    329
    +                            .width(250.dp)
    
    330
    +                            .height(currentWindowDpSize().height) // fixMaxHeight() doesn't work, use this instead
    
    331
    +                    ) {
    
    332
    +                        item {
    
    333
    +                            RegionDropdownMenuItem(
    
    334
    +                                "automatic",
    
    335
    +                                firstMenuItem.collectAsState().value,
    
    336
    +                                dismissAction = { expanded = false },
    
    337
    +                                textStyle = textStyle,
    
    338
    +                            )
    
    339
    +                        }
    
    340
    +                        if (torConnectionAssistViewModel.frequentRegionCodes.value?.isEmpty() == false) {
    
    341
    +                            item {
    
    342
    +                                HorizontalDivider(Modifier, DividerDefaults.Thickness, color = PhotonColors.Ink05)
    
    343
    +                                DropdownMenuItem(
    
    344
    +                                    text = {
    
    345
    +                                        Text(
    
    346
    +                                            text = getString(R.string.connection_assist_frequently_selected_locations),
    
    347
    +                                            style = labelTextStyle,
    
    348
    +                                        )
    
    349
    +                                    },
    
    350
    +                                    onClick = {},
    
    351
    +                                )
    
    352
    +                            }
    
    353
    +                            viewLifecycleOwner.lifecycleScope.launch {
    
    354
    +                                repeatOnLifecycle(Lifecycle.State.STARTED) {
    
    355
    +                                    torConnectionAssistViewModel.frequentRegionCodes.collect { codes ->
    
    356
    +                                        if (codes != null) {
    
    357
    +                                            for (code in codes) {
    
    358
    +                                                item {
    
    359
    +                                                    RegionDropdownMenuItem(
    
    360
    +                                                        code,
    
    361
    +                                                        torConnectionAssistViewModel.regionCodeNameMap.collectAsState().value?.get(
    
    362
    +                                                            code,
    
    363
    +                                                        ),
    
    364
    +                                                        dismissAction = { expanded = false },
    
    365
    +                                                        textStyle = textStyle,
    
    366
    +                                                    )
    
    367
    +                                                }
    
    368
    +                                            }
    
    369
    +                                        }
    
    370
    +                                    }
    
    371
    +                                }
    
    372
    +                            }
    
    373
    +                            item {
    
    374
    +                                HorizontalDivider(Modifier, DividerDefaults.Thickness, color = PhotonColors.Ink05)
    
    375
    +                                DropdownMenuItem(
    
    376
    +                                    text = {
    
    377
    +                                        Text(
    
    378
    +                                            text = getString(R.string.connection_assist_other_locations),
    
    379
    +                                            style = labelTextStyle,
    
    380
    +                                        )
    
    381
    +                                    },
    
    382
    +                                    onClick = {}
    
    383
    +                                )
    
    384
    +                            }
    
    385
    +                        }
    
    386
    +                        viewLifecycleOwner.lifecycleScope.launch {
    
    387
    +                            repeatOnLifecycle(Lifecycle.State.STARTED) {
    
    388
    +                                torConnectionAssistViewModel.regionCodeNameMap.collect { regions ->
    
    389
    +                                    if (regions != null) {
    
    390
    +                                        for (region in regions.toList()) {
    
    391
    +                                            item {
    
    392
    +                                                RegionDropdownMenuItem(
    
    393
    +                                                    region.first, region.second,
    
    394
    +                                                    dismissAction = { expanded = false },
    
    395
    +                                                    textStyle = textStyle,
    
    396
    +                                                )
    
    397
    +                                            }
    
    398
    +                                        }
    
    399
    +                                    }
    
    400
    +                                }
    
    401
    +                            }
    
    295 402
                             }
    
    296 403
                         }
    
    297
    -                }
    
    298 404
                 }
    
    299 405
             }
    
    406
    +    }
    
    300 407
     
    
    301
    -        return spinnerAdapter
    
    408
    +    @Composable
    
    409
    +    fun RegionDropdownMenuItem(
    
    410
    +        key: String,
    
    411
    +        text: String?,
    
    412
    +        dismissAction: () -> Unit,
    
    413
    +        textStyle: TextStyle,
    
    414
    +    ) {
    
    415
    +        DropdownMenuItem(
    
    416
    +            modifier = Modifier.padding(start = 8.dp),
    
    417
    +            text = { Text(
    
    418
    +                text ?: return@DropdownMenuItem,
    
    419
    +                style = textStyle,
    
    420
    +            ) },
    
    421
    +            onClick = {
    
    422
    +                torConnectionAssistViewModel.selectedCountryCode.value = key
    
    423
    +                updateButton1(torConnectionAssistViewModel.torConnectScreen.value)
    
    424
    +                dismissAction()
    
    425
    +            },
    
    426
    +        )
    
    302 427
         }
    
    303 428
     
    
    304 429
         private fun setButton1(screen: ConnectAssistUiState) {
    

  • mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tor/TorConnectionAssistViewModel.kt
    ... ... @@ -116,14 +116,6 @@ class TorConnectionAssistViewModel(
    116 116
             selectedCountryCode.value = torConnectStage.value?.defaultRegion ?: "automatic"
    
    117 117
         }
    
    118 118
     
    
    119
    -    fun setCountryCodeToSelectedItem(position: Int) {
    
    120
    -        selectedCountryCode.value =
    
    121
    -            regionCodeNameMap.value?.keys?.toList()
    
    122
    -                ?.getOrNull(position - 1) ?: "automatic"
    
    123
    -        // position - 1 since we have the default/first value of automatic
    
    124
    -        Log.d(TAG, "selectedCountryCode = ${selectedCountryCode.value}")
    
    125
    -    }
    
    126
    -
    
    127 119
         val shouldOpenHome: MutableLiveData<Boolean> by lazy {
    
    128 120
             MutableLiveData(false)
    
    129 121
         }
    

  • mobile/android/fenix/app/src/main/res/layout/fragment_tor_connection_assist.xml
    ... ... @@ -109,30 +109,14 @@
    109 109
             android:textColor="@color/photonLightGrey05"
    
    110 110
             app:layout_constraintTop_toBottomOf="@id/title_description" />
    
    111 111
     
    
    112
    -    <TextView
    
    113
    -        android:id="@+id/unblock_the_internet_in_country_description"
    
    112
    +    <androidx.compose.ui.platform.ComposeView
    
    113
    +        android:id="@+id/regionDropDown"
    
    114 114
             android:layout_width="match_parent"
    
    115 115
             android:layout_height="wrap_content"
    
    116
    -        android:paddingHorizontal="24dp"
    
    117
    -        android:layout_marginTop="24dp"
    
    118
    -        android:text="@string/connection_assist_unblock_the_internet_in_country_or_region"
    
    119
    -        android:textColor="@color/photonLightGrey05"
    
    120
    -        android:visibility="gone"
    
    121
    -        app:layout_constraintTop_toBottomOf="@id/title_description" />
    
    122
    -
    
    123
    -    <androidx.appcompat.widget.AppCompatSpinner
    
    124
    -        android:id="@+id/country_drop_down"
    
    125
    -        style="@style/Widget.AppCompat.Spinner.Underlined"
    
    126
    -        android:layout_width="match_parent"
    
    127
    -        android:layout_height="wrap_content"
    
    128
    -        android:layout_marginStart="24dp"
    
    129
    -        android:layout_marginTop="8dp"
    
    130
    -        android:layout_marginEnd="24dp"
    
    131
    -        android:textColor="@color/photonLightGrey05"
    
    132 116
             android:visibility="gone"
    
    133 117
             app:layout_constraintEnd_toEndOf="parent"
    
    134 118
             app:layout_constraintStart_toStartOf="parent"
    
    135
    -        app:layout_constraintTop_toBottomOf="@id/unblock_the_internet_in_country_description" />
    
    119
    +        app:layout_constraintTop_toBottomOf="@id/quickstart_switch" />
    
    136 120
     
    
    137 121
         <ImageView
    
    138 122
             android:id="@+id/wordmarkLogo"
    

  • mobile/android/fenix/app/src/main/res/values/torbrowser_strings.xml
    ... ... @@ -91,6 +91,10 @@
    91 91
         <!-- Connection assist, -->
    
    92 92
         <string name="connection_assist_select_country_or_region">Select country or region</string>
    
    93 93
         <!-- Connection assist. -->
    
    94
    +    <string name="connection_assist_frequently_selected_locations">Frequently selected locations</string>
    
    95
    +    <!-- Connection assist. -->
    
    96
    +    <string name="connection_assist_other_locations">Other locations</string>
    
    97
    +    <!-- Connection assist. -->
    
    94 98
         <string name="connection_assist_try_a_bridge_button">Try a bridge</string>
    
    95 99
         <!-- Connection assist. -->
    
    96 100
         <string name="connection_assist_trying_a_bridge_title">Trying a bridge…</string>
    

  • _______________________________________________
    tor-commits mailing list -- tor-commits@xxxxxxxxxxxxxxxxxxxx
    To unsubscribe send an email to tor-commits-leave@xxxxxxxxxxxxxxxxxxxx