diff --git a/res/layout/wifi_dpp_choose_saved_wifi_network_fragment.xml b/res/layout/wifi_dpp_choose_saved_wifi_network_fragment.xml index c3fccd0c5f1..ed288f06053 100644 --- a/res/layout/wifi_dpp_choose_saved_wifi_network_fragment.xml +++ b/res/layout/wifi_dpp_choose_saved_wifi_network_fragment.xml @@ -34,7 +34,7 @@ - diff --git a/res/xml/wifi_dpp_network_list.xml b/res/xml/wifi_dpp_network_list.xml new file mode 100644 index 00000000000..b128c7d8398 --- /dev/null +++ b/res/xml/wifi_dpp_network_list.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java b/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java index 8037e2305f8..5694ad66820 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java +++ b/src/com/android/settings/wifi/dpp/WifiDppChooseSavedWifiNetworkFragment.java @@ -25,6 +25,9 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.ListView; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; @@ -33,6 +36,8 @@ import com.android.settings.R; * {@code WifiDppConfiguratorActivity} to start with this fragment to choose a saved Wi-Fi network. */ public class WifiDppChooseSavedWifiNetworkFragment extends WifiDppQrCodeBaseFragment { + private static final String TAG_FRAGMENT_WIFI_NETWORK_LIST = "wifi_network_list_fragment"; + private ListView mSavedWifiNetworkList; private Button mButtonLeft; private Button mButtonRight; @@ -50,6 +55,15 @@ public class WifiDppChooseSavedWifiNetworkFragment extends WifiDppQrCodeBaseFrag if (actionBar != null) { actionBar.hide(); } + + /** Embeded WifiNetworkListFragment as child fragment within + * WifiDppChooseSavedWifiNetworkFragment. */ + final FragmentManager fragmentManager = getChildFragmentManager(); + final WifiNetworkListFragment fragment = new WifiNetworkListFragment(); + final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.wifi_network_list_container, fragment, + TAG_FRAGMENT_WIFI_NETWORK_LIST); + fragmentTransaction.commit(); } @Override diff --git a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java index 97ee71ab93e..51777c59518 100644 --- a/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java +++ b/src/com/android/settings/wifi/dpp/WifiDppConfiguratorActivity.java @@ -53,7 +53,9 @@ public class WifiDppConfiguratorActivity extends InstrumentedActivity implements WifiDppQrCodeGeneratorFragment.OnQrCodeGeneratorFragmentAddButtonClickedListener, WifiDppQrCodeScannerFragment.OnScanWifiDppSuccessListener, WifiDppQrCodeScannerFragment.OnScanZxingWifiFormatSuccessListener, - WifiDppAddDeviceFragment.OnClickChooseDifferentNetworkListener { + WifiDppAddDeviceFragment.OnClickChooseDifferentNetworkListener, + WifiNetworkListFragment.OnChooseNetworkListener { + private static final String TAG = "WifiDppConfiguratorActivity"; public static final String ACTION_CONFIGURATOR_QR_CODE_SCANNER = @@ -329,4 +331,11 @@ public class WifiDppConfiguratorActivity extends InstrumentedActivity implements super.onSaveInstanceState(outState); } + + @Override + public void onChooseNetwork(WifiNetworkConfig wifiNetworkConfig) { + mWifiNetworkConfig = new WifiNetworkConfig(wifiNetworkConfig); + + showAddDeviceFragment(/* addToBackStack */ true); + } } diff --git a/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java b/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java new file mode 100644 index 00000000000..13bb062f469 --- /dev/null +++ b/src/com/android/settings/wifi/dpp/WifiNetworkListFragment.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2019 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.dpp; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.wifi.AddNetworkFragment; +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.AccessPointPreference; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; + +import java.util.List; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +public class WifiNetworkListFragment extends SettingsPreferenceFragment implements + WifiTracker.WifiListener, AccessPoint.AccessPointListener { + private static final String TAG = "WifiNetworkListFragment"; + + private static final String WIFI_CONFIG_KEY = "wifi_config_key"; + private static final String PREF_KEY_EMPTY_WIFI_LIST = "wifi_empty_list"; + private static final String PREF_KEY_ACCESS_POINTS = "access_points"; + + static final int ADD_NETWORK_REQUEST = 1; + + private PreferenceCategory mAccessPointsPreferenceCategory; + private AccessPointPreference.UserBadgeCache mUserBadgeCache; + private Preference mAddPreference; + + private WifiManager mWifiManager; + private WifiTracker mWifiTracker; + + private WifiManager.ActionListener mSaveListener; + + // Container Activity must implement this interface + public interface OnChooseNetworkListener { + public void onChooseNetwork(WifiNetworkConfig wifiNetworkConfig); + } + + OnChooseNetworkListener mOnChooseNetworkListener; + + @Override + public int getMetricsCategory() { + return MetricsProto.MetricsEvent.SETTINGS_WIFI_DPP_CONFIGURATOR; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (!(context instanceof OnChooseNetworkListener)) { + throw new IllegalArgumentException("Invalid context type"); + } + mOnChooseNetworkListener = (OnChooseNetworkListener) context; + } + + @Override + public void onDetach() { + mOnChooseNetworkListener = null; + super.onDetach(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mWifiTracker = WifiTrackerFactory.create(getActivity(), this, + getSettingsLifecycle(), /* includeSaved */true, /* includeSaved */ true); + mWifiManager = mWifiTracker.getManager(); + + mSaveListener = new WifiManager.ActionListener() { + @Override + public void onSuccess() { + // Do nothing. + } + + @Override + public void onFailure(int reason) { + Activity activity = getActivity(); + if (activity != null) { + Toast.makeText(activity, + R.string.wifi_failed_save_message, + Toast.LENGTH_SHORT).show(); + } + } + }; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == ADD_NETWORK_REQUEST) { + if (resultCode == Activity.RESULT_OK) { + handleAddNetworkSubmitEvent(data); + } + mWifiTracker.resumeScanning(); + } + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.wifi_dpp_network_list); + + mAccessPointsPreferenceCategory = (PreferenceCategory) findPreference( + PREF_KEY_ACCESS_POINTS); + + mAddPreference = new Preference(getPrefContext()); + mAddPreference.setIcon(R.drawable.ic_menu_add); + mAddPreference.setTitle(R.string.wifi_add_network); + + mUserBadgeCache = new AccessPointPreference.UserBadgeCache(getPackageManager()); + } + + /** Called when the state of Wifi has changed. */ + @Override + public void onWifiStateChanged(int state) { + final int wifiState = mWifiManager.getWifiState(); + switch (wifiState) { + case WifiManager.WIFI_STATE_ENABLED: + updateAccessPointPreferences(); + break; + + case WifiManager.WIFI_STATE_ENABLING: + case WifiManager.WIFI_STATE_DISABLING: + removeAccessPointPreferences(); + break; + } + } + + /** Called when the connection state of wifi has changed. */ + @Override + public void onConnectedChanged() { + // Do nothing. + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * getAccessPoints should be called to get the latest information. + */ + @Override + public void onAccessPointsChanged() { + updateAccessPointPreferences(); + } + + @Override + public void onAccessPointChanged(final AccessPoint accessPoint) { + Log.d(TAG, "onAccessPointChanged (singular) callback initiated"); + View view = getView(); + if (view != null) { + view.post(() -> { + final Object tag = accessPoint.getTag(); + if (tag != null) { + ((AccessPointPreference) tag).refresh(); + } + }); + } + } + + @Override + public void onLevelChanged(AccessPoint accessPoint) { + ((AccessPointPreference) accessPoint.getTag()).onLevelChanged(); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + if (preference instanceof AccessPointPreference) { + final AccessPoint selectedAccessPoint = + ((AccessPointPreference) preference).getAccessPoint(); + if (selectedAccessPoint == null) { + return false; + } + + // Launch WifiDppAddDeviceFragment to start DPP in Configurator-Initiator role. + final WifiConfiguration wifiConfig = selectedAccessPoint.getConfig(); + if (wifiConfig == null) { + throw new IllegalArgumentException("Invalid access point"); + } + final WifiNetworkConfig networkConfig = WifiNetworkConfig.getValidConfigOrNull( + selectedAccessPoint.getSecurityString(/* concise */ true), + wifiConfig.getPrintableSsid(), wifiConfig.preSharedKey, /* hiddenSsid */ false, + wifiConfig.networkId); + if (mOnChooseNetworkListener != null) { + mOnChooseNetworkListener.onChooseNetwork(networkConfig); + } + } else if (preference == mAddPreference) { + launchAddNetworkFragment(); + } else { + return super.onPreferenceTreeClick(preference); + } + return true; + } + + private void handleAddNetworkSubmitEvent(Intent data) { + final WifiConfiguration wifiConfiguration = data.getParcelableExtra(WIFI_CONFIG_KEY); + if (wifiConfiguration != null) { + mWifiManager.save(wifiConfiguration, mSaveListener); + } + } + + private boolean isValidForDppConfiguration(AccessPoint accessPoint) { + final int security = accessPoint.getSecurity(); + + // DPP 1.0 only support SAE and PSK. + if (!(security == AccessPoint.SECURITY_PSK || security == AccessPoint.SECURITY_SAE)) { + return false; + } + + // Can only use saved network for DPP configuration. For ephemeral connections networkId + // is invalid. + if (!accessPoint.isSaved()) { + return false; + } + + // Ignore access points that are out of range. + if (!accessPoint.isReachable()) { + return false; + } + + return true; + } + + private void launchAddNetworkFragment() { + new SubSettingLauncher(getContext()) + .setTitleRes(R.string.wifi_add_network) + .setDestination(AddNetworkFragment.class.getName()) + .setSourceMetricsCategory(getMetricsCategory()) + .setResultListener(this, ADD_NETWORK_REQUEST) + .launch(); + } + + private void removeAccessPointPreferences() { + mAccessPointsPreferenceCategory.removeAll(); + mAccessPointsPreferenceCategory.setVisible(false); + } + + private void updateAccessPointPreferences() { + // in case state has changed + if (!mWifiManager.isWifiEnabled()) { + return; + } + + // AccessPoints are sorted by the WifiTracker + final List accessPoints = mWifiTracker.getAccessPoints(); + + boolean hasAvailableAccessPoints = false; + mAccessPointsPreferenceCategory.setVisible(true); + + cacheRemoveAllPrefs(mAccessPointsPreferenceCategory); + + int index = 0; + for (; index < accessPoints.size(); index++) { + AccessPoint accessPoint = accessPoints.get(index); + // Check if this access point is valid for DPP. + if (isValidForDppConfiguration(accessPoint)) { + final String key = accessPoint.getKey(); + hasAvailableAccessPoints = true; + final AccessPointPreference pref = (AccessPointPreference) getCachedPreference(key); + if (pref != null) { + pref.setOrder(index); + continue; + } + final AccessPointPreference preference = createAccessPointPreference(accessPoint); + preference.setKey(key); + preference.setOrder(index); + + mAccessPointsPreferenceCategory.addPreference(preference); + accessPoint.setListener(this); + preference.refresh(); + } + } + removeCachedPrefs(mAccessPointsPreferenceCategory); + mAddPreference.setOrder(index); + mAccessPointsPreferenceCategory.addPreference(mAddPreference); + + if (!hasAvailableAccessPoints) { + final Preference pref = new Preference(getPrefContext()); + pref.setSelectable(false); + pref.setSummary(R.string.wifi_empty_list_wifi_on); + pref.setOrder(index++); + pref.setKey(PREF_KEY_EMPTY_WIFI_LIST); + mAccessPointsPreferenceCategory.addPreference(pref); + } + } + + private AccessPointPreference createAccessPointPreference(AccessPoint accessPoint) { + return new AccessPointPreference(accessPoint, getPrefContext(), mUserBadgeCache, + R.drawable.ic_wifi_signal_0, /* forSavedNetworks */ false); + } +} diff --git a/tests/robotests/assets/grandfather_not_implementing_index_provider b/tests/robotests/assets/grandfather_not_implementing_index_provider index 447de0049df..015502ba3dd 100644 --- a/tests/robotests/assets/grandfather_not_implementing_index_provider +++ b/tests/robotests/assets/grandfather_not_implementing_index_provider @@ -80,3 +80,4 @@ com.android.settings.wifi.details.WifiNetworkDetailsFragment com.android.settings.wifi.p2p.WifiP2pSettings com.android.settings.wifi.savedaccesspoints.SavedAccessPointsWifiSettings com.android.settings.wifi.WifiInfo +com.android.settings.wifi.dpp.WifiNetworkListFragment diff --git a/tests/unit/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java b/tests/unit/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java new file mode 100644 index 00000000000..e09228dae93 --- /dev/null +++ b/tests/unit/src/com/android/settings/wifi/dpp/WifiNetworkListFragmentTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2019 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.dpp; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.net.wifi.WifiSsid; +import android.provider.Settings; + +import androidx.fragment.app.Fragment; +import androidx.test.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTracker.WifiListener; +import com.android.settingslib.wifi.WifiTrackerFactory; + +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class WifiNetworkListFragmentTest { + private static final String TEST_SSID = "\"Test Ssid\""; + private static final String TEST_UNQUOTED_SSID = "Test Ssid"; + private static final String TEST_BSSID = "0a:08:5c:67:89:00"; + private static final int TEST_RSSI = 123; + private static final int TEST_NETWORK_ID = 1; + + private static final String TEST_DPP_URL = "DPP:C:81/1;I:DPP_TESTER;K:" + + "MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgADUysZxnwRFGQR7Fepipjl84TG/dQR07es91iOB3PkPOk=;;"; + + // Keys used to lookup resources by name (see the resourceId/resourceString helper methods). + private static final String ID = "id"; + private static final String STRING = "string"; + private static final String WIFI_DISPLAY_STATUS_CONNECTED = "wifi_display_status_connected"; + + @Mock + private WifiTracker mWifiTracker; + @Mock + private WifiManager mWifiManager; + + private WifiNetworkListFragment mWifiNetworkListFragment; + private Context mContext; + + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>( + WifiDppConfiguratorActivity.class, true); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = InstrumentationRegistry.getTargetContext(); + WifiTrackerFactory.setTestingWifiTracker(mWifiTracker); + when(mWifiTracker.getManager()).thenReturn(mWifiManager); + } + + private void callOnWifiStateChanged(int state) { + mActivityRule.getActivity().getMainThreadHandler() + .post(() -> mWifiNetworkListFragment.onWifiStateChanged(state)); + } + + /** Launch the activity via an Intent with a String extra. */ + private void launchActivity(String extraName, String extraValue) { + final Intent intent = new Intent( + WifiDppConfiguratorActivity.ACTION_PROCESS_WIFI_DPP_QR_CODE); + if (extraName != null && extraValue != null) { + intent.putExtra(extraName, extraValue); + } + mActivityRule.launchActivity(intent); + + verify(mWifiTracker).getManager(); + + List fragments = + mActivityRule.getActivity().getSupportFragmentManager().getFragments(); + assertThat(fragments.size()).isEqualTo(1); + List childFragments = fragments.get(0).getChildFragmentManager().getFragments(); + assertThat(childFragments.size()).isEqualTo(1); + mWifiNetworkListFragment = (WifiNetworkListFragment) childFragments.get(0); + assertThat(mWifiNetworkListFragment).isNotNull(); + } + + private int resourceId(String type, String name) { + return mContext.getResources().getIdentifier(name, type, mContext.getPackageName()); + } + + /** Similar to {@link #resourceId}, but for accessing R.string. values. */ + private String resourceString(String name) { + return mContext.getResources().getString(resourceId(STRING, name)); + } + + private void setWifiState(int wifiState) { + when(mWifiManager.getWifiState()).thenReturn(wifiState); + when(mWifiManager.isWifiEnabled()).thenReturn(wifiState == WifiManager.WIFI_STATE_ENABLED); + } + + private void setupConnectedAccessPoint() { + final WifiConfiguration config = new WifiConfiguration(); + config.SSID = TEST_SSID; + config.BSSID = TEST_BSSID; + config.networkId = TEST_NETWORK_ID; + config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK); + final WifiInfo wifiInfo = new WifiInfo(); + wifiInfo.setSSID(WifiSsid.createFromAsciiEncoded(TEST_UNQUOTED_SSID)); + wifiInfo.setBSSID(TEST_BSSID); + wifiInfo.setRssi(TEST_RSSI); + wifiInfo.setNetworkId(TEST_NETWORK_ID); + final NetworkInfo networkInfo = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, null, null); + networkInfo.setDetailedState(NetworkInfo.DetailedState.CONNECTED, null, null); + final AccessPoint accessPoint = new AccessPoint(mContext, config); + accessPoint.update(config, wifiInfo, networkInfo); + + assertThat(accessPoint.getSsidStr()).isEqualTo(TEST_UNQUOTED_SSID); + assertThat(accessPoint.getBssid()).isEqualTo(TEST_BSSID); + assertThat(accessPoint.getNetworkInfo()).isNotNull(); + assertThat(accessPoint.isActive()).isTrue(); + assertThat(accessPoint.getSettingsSummary()).isEqualTo( + resourceString(WIFI_DISPLAY_STATUS_CONNECTED)); + + when(mWifiTracker.getAccessPoints()).thenReturn( + Lists.asList(accessPoint, new AccessPoint[]{})); + } + + @Test + public void onConnected_shouldSeeConnectedMessage() throws Exception { + setWifiState(WifiManager.WIFI_STATE_ENABLED); + setupConnectedAccessPoint(); + when(mWifiTracker.isConnected()).thenReturn(true); + + launchActivity(WifiDppUtils.EXTRA_QR_CODE, TEST_DPP_URL); + callOnWifiStateChanged(WifiManager.WIFI_STATE_ENABLED); + + onView(withText(resourceString(WIFI_DISPLAY_STATUS_CONNECTED))).check( + matches(isDisplayed())); + } +} \ No newline at end of file