Adding share toggle to the network details page

Adds the share toggle and checks if there is a conflict when user
attempts to change the toggle. In case of a conflict, an alert dialog in shown
to the user.

Bug: 409845756

Flag: com.android.settings.connectivity.wifi_multiuser

Test: Manual testing
Change-Id: Ia57b967ad933ebb19f27f1e50d6a69210b84ac4c
This commit is contained in:
Nikhil Nayunigari
2025-05-31 01:21:04 +00:00
committed by Joey
parent 9bc9820a22
commit f7d5ecee9a
5 changed files with 318 additions and 0 deletions

View File

@@ -2569,6 +2569,22 @@
<string name="retry">Retry</string>
<!-- Label for the check box to share a network with other users on the same device -->
<string name="wifi_shared">Share with other device users</string>
<!-- Label for the shared wifi message -->
<string name="shared_message">shared</string>
<!-- Label for the private wifi message -->
<string name="private_message">private</string>
<!-- Label for the conflict wifi dialog confirm button -->
<string name="wifi_conflict_dialog_confirm">Confirm</string>
<!-- Label for the conflict wifi dialog cancel button -->
<string name="wifi_conflict_dialog_cancel">Cancel</string>
<!-- Title for the preference to share a network with other users on the same device -->
<string name="wifi_share_preference_title">This is a shared network</string>
<!-- Summary for the preference to share a network with other users on the same device -->
<string name="wifi_share_preference_summary">All users with the exception of guest users, will be able to view, edit and delete this network</string>
<!-- Alert dialog title, informing the user to switch to the conflicting network. [CHAR LIMIT=250] -->
<string name="wifi_conflict_dialog_title">Switch to <xliff:g id="shared">%1$s</xliff:g> \"<xliff:g id="network_name">%2$s</xliff:g>\" network?</string>
<!-- Alert dialog summary, informing the user to switch to the conflicting network. [CHAR LIMIT=250] -->
<string name="wifi_conflict_dialog_message">You\'re currently connected to a <xliff:g id="shared">%1$s</xliff:g> network with the same name. When you switch, you\'ll disconnect from the <xliff:g id="shared">%2$s</xliff:g> network and then reconnect to the <xliff:g id="private">%3$s</xliff:g> network. You may temporarily have no internet.</string>
<!-- Hint for unchanged fields -->
<string name="wifi_unchanged">(unchanged)</string>
<!-- Label for the check box to allow other device users to edit network details -->

View File

@@ -102,6 +102,12 @@
android:entries="@array/wifi_privacy_entries_ext"
android:entryValues="@array/wifi_privacy_values_ext"/>
<SwitchPreferenceCompat
android:key="shared"
android:icon="@drawable/ic_edit_24dp"
android:title="@string/wifi_share_preference_title"
android:summary="@string/wifi_share_preference_summary"/>
<SwitchPreferenceCompat
android:key="edit_configuration"
android:icon="@drawable/ic_edit_24dp"

View File

@@ -55,6 +55,7 @@ import com.android.settings.overlay.FeatureFactory;
import com.android.settings.wifi.WepLessSecureWarningController;
import com.android.settings.wifi.WifiConfigUiBase2;
import com.android.settings.wifi.WifiDialog2;
import com.android.settings.wifi.WifiPickerTrackerHelper;
import com.android.settings.wifi.WifiUtils;
import com.android.settings.wifi.details2.AddDevicePreferenceController2;
import com.android.settings.wifi.details2.CertificateDetailsPreferenceController;
@@ -66,6 +67,7 @@ import com.android.settings.wifi.details2.WifiMeteredPreferenceController2;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2;
import com.android.settings.wifi.details2.WifiSecondSummaryController2;
import com.android.settings.wifi.details2.WifiSharedPreferenceController;
import com.android.settings.wifi.details2.WifiSubscriptionDetailPreferenceController2;
import com.android.settings.wifi.repository.SharedConnectivityRepository;
import com.android.settingslib.RestrictedLockUtils;
@@ -100,6 +102,7 @@ public class WifiNetworkDetailsFragment extends RestrictedDashboardFragment impl
public static final String KEY_HOTSPOT_DEVICE_BATTERY = "hotspot_device_details_battery";
public static final String KEY_HOTSPOT_CONNECTION_CATEGORY = "hotspot_connection_category";
public static final String KEY_EDIT_CONFIG_TOGGLE = "edit_configuration";
public static final String KEY_SHARED_TOGGLE = "shared";
// Max age of tracked WifiEntries
private static final long MAX_SCAN_AGE_MILLIS = 15_000;
@@ -116,6 +119,8 @@ public class WifiNetworkDetailsFragment extends RestrictedDashboardFragment impl
private List<WifiDialog2.WifiDialog2Listener> mWifiDialogListeners = new ArrayList<>();
@VisibleForTesting
List<AbstractPreferenceController> mControllers;
@VisibleForTesting
WifiPickerTrackerHelper mWifiPickerTrackerHelper;
private boolean mIsInstantHotspotFeatureEnabled =
SharedConnectivityRepository.isDeviceConfigEnabled();
@VisibleForTesting
@@ -289,6 +294,13 @@ public class WifiNetworkDetailsFragment extends RestrictedDashboardFragment impl
context, KEY_EDIT_CONFIG_TOGGLE, wifiEntry);
mControllers.add(wifiEditConfigPreferenceController);
mWifiPickerTrackerHelper =
new WifiPickerTrackerHelper(getSettingsLifecycle(), getContext(), null);
final WifiSharedPreferenceController wifiSharedPreferenceController =
new WifiSharedPreferenceController(
context, KEY_SHARED_TOGGLE, mWifiPickerTrackerHelper, wifiEntry);
mControllers.add(wifiSharedPreferenceController);
final AddDevicePreferenceController2 addDevicePreferenceController2 =
new AddDevicePreferenceController2(context);
addDevicePreferenceController2.setWifiEntry(wifiEntry);

View File

@@ -0,0 +1,122 @@
/*
* 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.wifi.details2
import android.content.Context
import android.content.DialogInterface
import android.net.wifi.WifiConfiguration
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import com.android.settings.R
import com.android.settings.core.SubSettingLauncher
import com.android.settings.core.TogglePreferenceController
import com.android.settings.wifi.WifiPickerTrackerHelper
import com.android.settings.wifi.details.WifiNetworkDetailsFragment
import com.android.wifitrackerlib.WifiEntry
class WifiSharedPreferenceController(
context: Context,
preferenceKey: String,
private val wifiPickerTrackerHelper: WifiPickerTrackerHelper,
private val wifiEntry: WifiEntry,
) : TogglePreferenceController(context, preferenceKey) {
override fun getAvailabilityStatus(): Int {
return if (com.android.settings.connectivity.Flags.wifiMultiuser()) {
AVAILABLE
} else {
CONDITIONALLY_UNAVAILABLE
}
}
override fun isChecked(): Boolean {
return wifiEntry.isSharedWithOtherUsers()
}
override fun setChecked(isChecked: Boolean): Boolean {
val matchingWifiEntry: WifiEntry? = getMatchingWifiEntry(isChecked)
var wifiConfiguration: WifiConfiguration? = matchingWifiEntry?.getWifiConfiguration()
if (matchingWifiEntry != null && wifiConfiguration != null) {
showAlertDialog(
matchingWifiEntry.ssid ?: "",
wifiConfiguration.shared,
matchingWifiEntry.getKey(),
)
}
return true
}
override fun getSliceHighlightMenuRes(): Int {
return R.string.menu_key_network
}
private fun showAlertDialog(ssid: String, shared: Boolean, key: String) {
AlertDialog.Builder(mContext)
.setTitle(
mContext.getString(
R.string.wifi_conflict_dialog_title,
if (shared) mContext.getString(R.string.shared_message)
else mContext.getString(R.string.private_message),
ssid,
)
)
.setMessage(
mContext.getString(
R.string.wifi_conflict_dialog_message,
if (shared) mContext.getString(R.string.private_message)
else mContext.getString(R.string.shared_message),
if (shared) mContext.getString(R.string.private_message)
else mContext.getString(R.string.shared_message),
if (shared) mContext.getString(R.string.shared_message)
else mContext.getString(R.string.private_message),
)
)
.setPositiveButton(mContext.getString(R.string.wifi_conflict_dialog_confirm)) {
dialog: DialogInterface,
which: Int ->
val bundle: Bundle = Bundle()
bundle.putString(WifiNetworkDetailsFragment.KEY_CHOSEN_WIFIENTRY_KEY, key)
SubSettingLauncher(mContext)
.setTitleText(mContext.getText(R.string.pref_title_network_details))
.setDestination(WifiNetworkDetailsFragment::class.java.name)
.setArguments(bundle)
.setSourceMetricsCategory(metricsCategory)
.launch()
}
.setNegativeButton(mContext.getString(R.string.wifi_conflict_dialog_cancel)) {
dialog: DialogInterface,
which: Int ->
dialog.dismiss()
}
.show()
}
private fun getMatchingWifiEntry(shared: Boolean): WifiEntry? {
val wifiPickerTracker = wifiPickerTrackerHelper.getWifiPickerTracker()
val matchingWifiEntry: WifiEntry? =
wifiPickerTracker.wifiEntries
.stream()
.filter { entry: WifiEntry -> TextUtils.equals(wifiEntry.ssid, entry.ssid) }
.filter { entry: WifiEntry -> entry.getWifiConfiguration()?.shared == shared }
.findFirst()
.orElse(null)
return matchingWifiEntry
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.wifi.details2
import android.content.Context
import android.net.wifi.WifiConfiguration
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.appcompat.app.AlertDialog
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settings.connectivity.Flags
import com.android.settings.core.BasePreferenceController
import com.android.settings.testutils.shadow.ShadowAlertDialogCompat
import com.android.settings.wifi.WifiPickerTrackerHelper
import com.android.wifitrackerlib.WifiEntry
import com.android.wifitrackerlib.WifiPickerTracker
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowDialog
import org.robolectric.shadows.ShadowLooper
@RunWith(AndroidJUnit4::class)
@Config(shadows = [ShadowAlertDialogCompat::class])
class WifiSharedPreferenceControllerTest {
@get:Rule val setFlagsRule = SetFlagsRule()
private var mockWifiEntry = mock<WifiEntry>()
private var mockWifiConfiguration = mock<WifiConfiguration>()
private val context: Context = spy(RuntimeEnvironment.application)
private val mockWifiPickerTracker: WifiPickerTracker = mock<WifiPickerTracker>()
private val mockWifiPickerTrackerHelper: WifiPickerTrackerHelper =
mock<WifiPickerTrackerHelper>()
private var controller =
WifiSharedPreferenceController(
context,
"share_configuration",
mockWifiPickerTrackerHelper,
mockWifiEntry,
)
@Before
fun setUp() {
mockWifiEntry.stub { on { getWifiConfiguration() } doReturn mockWifiConfiguration }
mockWifiEntry.stub { on { getSsid() } doReturn "testSSID" }
mockWifiEntry.stub { on { getKey() } doReturn "testKey" }
context.setTheme(R.style.Theme_Settings_Home)
}
@Test
fun isChecked_returnsWifiEntry_allowEditConfig_Value() {
mockWifiEntry.stub { on { isSharedWithOtherUsers() } doReturn false }
assertThat(controller.isChecked()).isFalse()
}
@Test
fun setChecked_conflictingEntry_showsAlertDialog_validateMessages() {
mockWifiConfiguration.shared = true
mockWifiPickerTrackerHelper.stub {
on { getWifiPickerTracker() } doReturn mockWifiPickerTracker
}
whenever(mockWifiPickerTracker.wifiEntries).thenReturn(listOf(mockWifiEntry))
ShadowDialog.reset()
controller.setChecked(true)
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
val shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog)
assertThat(shadowDialog).isNotNull()
assertThat(shadowDialog.getTitle().toString())
.isEqualTo(context.getString(R.string.wifi_conflict_dialog_title, "shared", "testSSID"))
assertThat(shadowDialog.getMessage().toString())
.isEqualTo(
context.getString(
R.string.wifi_conflict_dialog_message,
"private",
"private",
"shared",
)
)
}
@Test
fun setChecked_conflictingEntry_showsAlertDialog_clickNegativeButton() {
mockWifiConfiguration.shared = true
mockWifiPickerTrackerHelper.stub {
on { getWifiPickerTracker() } doReturn mockWifiPickerTracker
}
whenever(mockWifiPickerTracker.wifiEntries).thenReturn(listOf(mockWifiEntry))
ShadowDialog.reset()
controller.setChecked(true)
var dialog: AlertDialog? = ShadowAlertDialogCompat.getLatestAlertDialog()
assertThat(dialog).isNotNull()
dialog?.getButton(AlertDialog.BUTTON_NEGATIVE)?.performClick()
ShadowLooper.idleMainLooper()
assertThat(dialog?.isShowing()).isFalse()
}
@Test
fun setChecked_noConflict_doesNotShowAlertDialog() {
mockWifiConfiguration.shared = true
mockWifiPickerTrackerHelper.stub {
on { getWifiPickerTracker() } doReturn mockWifiPickerTracker
}
whenever(mockWifiPickerTracker.wifiEntries).thenReturn(listOf())
ShadowDialog.reset()
controller.setChecked(true)
val dialog = ShadowAlertDialogCompat.getLatestAlertDialog()
assertThat(dialog).isNull()
}
@Test
@DisableFlags(Flags.FLAG_WIFI_MULTIUSER)
fun getAvailabilityStatus_flagDisabled() {
assertThat(controller.getAvailabilityStatus())
.isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE)
}
@Test
@EnableFlags(Flags.FLAG_WIFI_MULTIUSER)
fun getAvailabilityStatus_flagEnabled() {
assertThat(controller.getAvailabilityStatus()).isEqualTo(BasePreferenceController.AVAILABLE)
}
}