diff --git a/res/layout/wifi_add_app_networks.xml b/res/layout/wifi_add_app_networks.xml index 2dc3993a85f..8696e04d43e 100644 --- a/res/layout/wifi_add_app_networks.xml +++ b/res/layout/wifi_add_app_networks.xml @@ -84,6 +84,42 @@ + + + + + + + + + + + - \ No newline at end of file + diff --git a/res/values/strings.xml b/res/values/strings.xml index 69152ddc90a..63dee46c6e1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2336,10 +2336,16 @@ AndroidAP WPA2 PSK hotspot AndroidHotspot - + Save this network? - + %1$s would like to save a network to your phone + + Saving\u2026 + + Saved + + Couldn\u2019t save. Try again. diff --git a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksActivity.java b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksActivity.java index 0810b54a7fa..298857ec3b4 100644 --- a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksActivity.java +++ b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksActivity.java @@ -18,6 +18,7 @@ package com.android.settings.wifi.addappnetworks; import android.content.Intent; import android.os.Bundle; +import android.provider.Settings; import android.view.Gravity; import android.view.Window; import android.view.WindowManager; @@ -53,6 +54,12 @@ public class AddAppNetworksActivity extends FragmentActivity { setContentView(R.layout.settings_panel); showAddNetworksFragment(); getLifecycle().addObserver(new HideNonSystemOverlayMixin(this)); + + // Move the window to the bottom of screen, and make it take up the entire screen width. + final Window window = getWindow(); + window.setGravity(Gravity.BOTTOM); + window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT); } @Override @@ -64,14 +71,11 @@ public class AddAppNetworksActivity extends FragmentActivity { @VisibleForTesting void showAddNetworksFragment() { - // Move the window to the bottom of screen, and make it take up the entire screen width. - final Window window = getWindow(); - window.setGravity(Gravity.BOTTOM); - window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.WRAP_CONTENT); - - // TODO: check the new intent status + // TODO: Check the new intent status. mBundle.putString(KEY_CALLING_PACKAGE_NAME, getCallingPackage()); + mBundle.putParcelableArrayList(Settings.EXTRA_WIFI_CONFIGURATION_LIST, + getIntent().getParcelableArrayListExtra(Settings.EXTRA_WIFI_CONFIGURATION_LIST)); + final FragmentManager fragmentManager = getSupportFragmentManager(); if (fragmentManager.findFragmentByTag(TAG) == null) { final AddAppNetworksFragment fragment = new AddAppNetworksFragment(); diff --git a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java index 281ca5780fe..eeec1660df3 100644 --- a/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java +++ b/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragment.java @@ -16,13 +16,22 @@ package com.android.settings.wifi.addappnetworks; +import static android.app.Activity.RESULT_CANCELED; +import static android.app.Activity.RESULT_OK; + import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiConfiguration.KeyMgmt; +import android.net.wifi.WifiManager; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -41,6 +50,7 @@ import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; import java.util.ArrayList; +import java.util.List; /** * The Fragment list those networks, which is proposed by other app, to user, and handle user's @@ -49,10 +59,36 @@ import java.util.ArrayList; public class AddAppNetworksFragment extends InstrumentedFragment { public static final String TAG = "AddAppNetworksFragment"; - private TextView mTitleView; - private TextView mSummaryView; - private ImageView mIconView; + // Security types of a requested or saved network. + private static final String SECURITY_NO_PASSWORD = "nopass"; + private static final String SECURITY_WEP = "wep"; + private static final String SECURITY_WPA_PSK = "wpa"; + private static final String SECURITY_SAE = "sae"; + // Possible result values in each item of the returned result list, which is used + // to inform the caller APP the processed result of each specified network. + private static final int RESULT_NETWORK_INITIAL = -1; //initial value + private static final int RESULT_NETWORK_SUCCESS = 0; + private static final int RESULT_NETWORK_ADD_ERROR = 1; + private static final int RESULT_NETWORK_ALREADY_EXISTS = 2; + + // Handler messages for controlling different state and delay showing the status message. + private static final int MESSAGE_START_SAVING_NETWORK = 1; + private static final int MESSAGE_SHOW_SAVED_AND_CONNECT_NETWORK = 2; + private static final int MESSAGE_SHOW_SAVE_FAILED = 3; + private static final int MESSAGE_FINISH = 4; + + // Signal level for the constant signal icon. + private static final int MAX_RSSI_SIGNAL_LEVEL = 4; + + // Duration for showing different status message. + private static final long SHOW_SAVING_INTERVAL_MILLIS = 500L; + private static final long SHOW_SAVED_INTERVAL_MILLIS = 1000L; + + @VisibleForTesting + FragmentActivity mActivity; + @VisibleForTesting + View mLayoutView; @VisibleForTesting Button mCancelButton; @VisibleForTesting @@ -60,45 +96,251 @@ public class AddAppNetworksFragment extends InstrumentedFragment { @VisibleForTesting String mCallingPackageName; + private TextView mSummaryView; + private TextView mSingleNetworkProcessingStatusView; + private int mSavingIndex; + private List mAllSpecifiedNetworksList; + private ArrayList mResultCodeArrayList; + private WifiManager.ActionListener mSaveListener; + private WifiManager mWifiManager; + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_START_SAVING_NETWORK: + mSaveButton.setEnabled(false); + // Set the initial text color for status message. + mSingleNetworkProcessingStatusView.setTextColor( + com.android.settingslib.Utils.getColorAttr(mActivity, + android.R.attr.textColorSecondary)); + mSingleNetworkProcessingStatusView.setText( + getString(R.string.wifi_add_app_single_network_saving_summary)); + mSingleNetworkProcessingStatusView.setVisibility(View.VISIBLE); + + // Save the proposed network. + saveNetworks(); + break; + + case MESSAGE_SHOW_SAVED_AND_CONNECT_NETWORK: + mSingleNetworkProcessingStatusView.setText( + getString(R.string.wifi_add_app_single_network_saved_summary)); + + // For the single network case, we need to call connection after saved. + connectNetwork(); + + sendEmptyMessageDelayed(MESSAGE_FINISH, + SHOW_SAVED_INTERVAL_MILLIS); + break; + + case MESSAGE_SHOW_SAVE_FAILED: + mSingleNetworkProcessingStatusView.setText( + getString(R.string.wifi_add_app_single_network_save_failed_summary)); + // Error message need to use colorError attribute to show. + mSingleNetworkProcessingStatusView.setTextColor( + com.android.settingslib.Utils.getColorAttr(mActivity, + android.R.attr.colorError)); + mSaveButton.setEnabled(true); + break; + + case MESSAGE_FINISH: + finishWithResult(RESULT_OK, mResultCodeArrayList); + break; + + default: + // Do nothing. + break; + } + } + }; + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + mActivity = getActivity(); + mWifiManager = mActivity.getSystemService(WifiManager.class); + return inflater.inflate(R.layout.wifi_add_app_networks, container, false); } + // TODO: Makesure function work correctly after rotate. @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - // init local variable. - mTitleView = view.findViewById(R.id.app_title); - mIconView = view.findViewById(R.id.app_icon); + // Initial UI variable. + mLayoutView = view; mCancelButton = view.findViewById(R.id.cancel); mSaveButton = view.findViewById(R.id.save); mSummaryView = view.findViewById(R.id.app_summary); - - // Assigns button listeners. + mSingleNetworkProcessingStatusView = view.findViewById(R.id.single_status); + // Assigns button listeners and network save listener. mCancelButton.setOnClickListener(getCancelListener()); mSaveButton.setOnClickListener(getSaveListener()); + prepareSaveResultListener(); + // Prepare the non-UI variables. final Bundle bundle = getArguments(); createContent(bundle); } private void createContent(Bundle bundle) { - final FragmentActivity activity = getActivity(); + mAllSpecifiedNetworksList = + bundle.getParcelableArrayList(Settings.EXTRA_WIFI_CONFIGURATION_LIST); - // Assigns caller app icon and summary. + // If there is no networks in the request intent, then just finish activity. + if (mAllSpecifiedNetworksList == null || mAllSpecifiedNetworksList.isEmpty()) { + finishWithResult(RESULT_CANCELED, null /* resultArrayList */); + return; + } + + // Initial the result arry. + initializeResultCodeArray(); + + // Filter out the saved networks, don't show saved networks to user. + checkSavedNetworks(); + + if (mAllSpecifiedNetworksList.size() == 1) { + // If the only one requested network is already saved, just return with existence. + if (mResultCodeArrayList.get(0) == RESULT_NETWORK_ALREADY_EXISTS) { + finishWithResult(RESULT_OK, mResultCodeArrayList); + return; + } + + // Show signal icon for single network case. + setSingleNetworkSignalIcon(); + // Show the SSID of the proposed network. + ((TextView) mLayoutView.findViewById(R.id.single_ssid)).setText( + mAllSpecifiedNetworksList.get(0).SSID); + // Set the status view as gone when UI is initialized. + mSingleNetworkProcessingStatusView.setVisibility(View.GONE); + } else { + // TODO: Add code for processing multiple networks case. + } + + // Assigns caller app icon, title, and summary. mCallingPackageName = bundle.getString(AddAppNetworksActivity.KEY_CALLING_PACKAGE_NAME); - assignAppIcon(activity, mCallingPackageName); - assignTitleAndSummary(activity, mCallingPackageName); + assignAppIcon(mActivity, mCallingPackageName); + assignTitleAndSummary(mActivity, mCallingPackageName); + } + + private void initializeResultCodeArray() { + final int networksSize = mAllSpecifiedNetworksList.size(); + mResultCodeArrayList = new ArrayList<>(); + + for (int i = 0; i < networksSize; i++) { + mResultCodeArrayList.add(RESULT_NETWORK_INITIAL); + } + } + + /** + * Classify security type into following types: + * 1. {@Code SECURITY_NO_PASSWORD}: No password network or OWE network. + * 2. {@Code SECURITY_WEP}: Traditional WEP encryption network. + * 3. {@Code SECURITY_WPA_PSK}: WPA/WPA2 preshare key type. + * 4. {@Code SECURITY_SAE}: SAE type network. + */ + private String getSecurityType(WifiConfiguration config) { + if (config.allowedKeyManagement.get(KeyMgmt.SAE)) { + return SECURITY_SAE; + } + if (config.allowedKeyManagement.get(KeyMgmt.OWE)) { + return SECURITY_NO_PASSWORD; + } + if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK) || config.allowedKeyManagement.get( + KeyMgmt.WPA2_PSK)) { + return SECURITY_WPA_PSK; + } + return (config.wepKeys[0] == null) ? SECURITY_NO_PASSWORD : SECURITY_WEP; + } + + /** + * For the APP specified networks, need to filter out those saved ones and mark them as existed. + */ + private void checkSavedNetworks() { + final List privilegedWifiConfigurations = + mWifiManager.getPrivilegedConfiguredNetworks(); + boolean foundInSavedList; + int networkPositionInBundle = 0; + for (WifiConfiguration specifiecConfig : mAllSpecifiedNetworksList) { + foundInSavedList = false; + final String ssidWithQuotation = addQuotationIfNeeded(specifiecConfig.SSID); + final String securityType = getSecurityType(specifiecConfig); + + for (WifiConfiguration privilegedWifiConfiguration : privilegedWifiConfigurations) { + final String savedSecurityType = getSecurityType(privilegedWifiConfiguration); + + // If SSID or security type is different, should be new network or need to updated + // network. + if (!ssidWithQuotation.equals(privilegedWifiConfiguration.SSID) + || !securityType.equals(savedSecurityType)) { + continue; + } + + // If specified network and saved network have same security types, we'll check + // more information according to their security type to judge if they are same. + switch (securityType) { + case SECURITY_NO_PASSWORD: + foundInSavedList = true; + break; + case SECURITY_WEP: + if (specifiecConfig.wepKeys[0].equals( + privilegedWifiConfiguration.wepKeys[0])) { + foundInSavedList = true; + } + break; + case SECURITY_WPA_PSK: + case SECURITY_SAE: + if (specifiecConfig.preSharedKey.equals( + privilegedWifiConfiguration.preSharedKey)) { + foundInSavedList = true; + } + break; + default: + break; + } + } + + if (foundInSavedList) { + // If this requested network already in the saved networks, mark this item in the + // result code list as existed. + mResultCodeArrayList.set(networkPositionInBundle, RESULT_NETWORK_ALREADY_EXISTS); + } else { + // TODO: for multiple networks case, need to add to adapter for present list to user + } + networkPositionInBundle++; + } + } + + private void setSingleNetworkSignalIcon() { + // TODO: Check level of the network to show signal icon. + final Drawable wifiIcon = mActivity.getDrawable( + Utils.getWifiIconResource(MAX_RSSI_SIGNAL_LEVEL)).mutate(); + final Drawable wifiIconDark = wifiIcon.getConstantState().newDrawable().mutate(); + wifiIconDark.setTintList( + Utils.getColorAttr(mActivity, android.R.attr.colorControlNormal)); + ((ImageView) mLayoutView.findViewById(R.id.signal_strength)).setImageDrawable(wifiIconDark); + } + + private String addQuotationIfNeeded(String input) { + if (TextUtils.isEmpty(input)) { + return ""; + } + + if (input.length() >= 2 && input.startsWith("\"") && input.endsWith("\"")) { + return input; + } + + StringBuilder sb = new StringBuilder(); + sb.append("\"").append(input).append("\""); + return sb.toString(); } private void assignAppIcon(Context context, String callingPackageName) { final Drawable drawable = loadPackageIconDrawable(context, callingPackageName); - mIconView.setImageDrawable(drawable); + ((ImageView) mLayoutView.findViewById(R.id.app_icon)).setImageDrawable(drawable); } private Drawable loadPackageIconDrawable(Context context, String callingPackageName) { @@ -114,7 +356,7 @@ public class AddAppNetworksFragment extends InstrumentedFragment { private void assignTitleAndSummary(Context context, String callingPackageName) { // Assigns caller app name to title - mTitleView.setText(getTitle()); + ((TextView) mLayoutView.findViewById(R.id.app_title)).setText(getTitle()); // Set summary mSummaryView.setText(getAddNetworkRequesterSummary( @@ -132,23 +374,60 @@ public class AddAppNetworksFragment extends InstrumentedFragment { View.OnClickListener getCancelListener() { return (v) -> { Log.d(TAG, "User rejected to add network"); + finishWithResult(RESULT_CANCELED, null /* resultArrayList */); }; } View.OnClickListener getSaveListener() { return (v) -> { Log.d(TAG, "User agree to add networks"); + // Start to process saving networks. + final Message message = mHandler.obtainMessage(MESSAGE_START_SAVING_NETWORK); + message.sendToTarget(); }; } + private void prepareSaveResultListener() { + mSaveListener = new WifiManager.ActionListener() { + @Override + public void onSuccess() { + mResultCodeArrayList.set(mSavingIndex, RESULT_NETWORK_SUCCESS); + Message nextState_Message = mHandler.obtainMessage( + MESSAGE_SHOW_SAVED_AND_CONNECT_NETWORK); + // Delay to change to next state for showing saving mesage for a period. + mHandler.sendMessageDelayed(nextState_Message, SHOW_SAVING_INTERVAL_MILLIS); + } + + @Override + public void onFailure(int reason) { + mResultCodeArrayList.set(mSavingIndex, RESULT_NETWORK_ADD_ERROR); + Message nextState_Message = mHandler.obtainMessage(MESSAGE_SHOW_SAVE_FAILED); + // Delay to change to next state for showing saving mesage for a period. + mHandler.sendMessageDelayed(nextState_Message, SHOW_SAVING_INTERVAL_MILLIS); + } + }; + } + + private void saveNetworks() { + final WifiConfiguration wifiConfiguration = mAllSpecifiedNetworksList.get(0); + wifiConfiguration.SSID = addQuotationIfNeeded(wifiConfiguration.SSID); + mWifiManager.save(wifiConfiguration, mSaveListener); + } + + private void connectNetwork() { + final WifiConfiguration wifiConfiguration = mAllSpecifiedNetworksList.get(0); + // Don't need to handle the connect result. + mWifiManager.connect(wifiConfiguration, null /* ActionListener */); + } + private void finishWithResult(int resultCode, ArrayList resultArrayList) { if (resultArrayList != null) { Intent intent = new Intent(); intent.putIntegerArrayListExtra(Settings.EXTRA_WIFI_CONFIGURATION_RESULT_LIST, resultArrayList); - getActivity().setResult(resultCode, intent); + mActivity.setResult(resultCode, intent); } - getActivity().finish(); + mActivity.finish(); } @Override diff --git a/tests/robotests/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragmentTest.java b/tests/robotests/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragmentTest.java index 38ddf335051..999d44ece92 100644 --- a/tests/robotests/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragmentTest.java +++ b/tests/robotests/src/com/android/settings/wifi/addappnetworks/AddAppNetworksFragmentTest.java @@ -22,49 +22,112 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import android.content.Context; +import android.net.wifi.WifiConfiguration; import android.os.Bundle; +import android.os.Parcelable; +import android.provider.Settings; +import android.widget.TextView; -import androidx.fragment.app.FragmentActivity; +import com.android.settings.R; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.androidx.fragment.FragmentController; +import java.util.ArrayList; +import java.util.List; + @RunWith(RobolectricTestRunner.class) public class AddAppNetworksFragmentTest { private static final String FAKE_APP_NAME = "fake_app_name"; - private FragmentActivity mActivity; + private static final String FAKE_SSID = "fake_ssid"; private AddAppNetworksFragment mAddAppNetworksFragment; private Context mContext; @Before public void setUp() { mAddAppNetworksFragment = spy(new AddAppNetworksFragment()); - MockitoAnnotations.initMocks(this); - - // Set up bundle - final Bundle bundle = new Bundle(); - bundle.putString(AddAppNetworksActivity.KEY_CALLING_PACKAGE_NAME, FAKE_APP_NAME); - doReturn(bundle).when(mAddAppNetworksFragment).getArguments(); - - FragmentController.setupFragment(mAddAppNetworksFragment); } @Test public void callingPackageName_onCreateView_shouldBeCorrect() { + setUpOneNetworkBundle(); + setupFragment(); + assertThat(mAddAppNetworksFragment.mCallingPackageName).isEqualTo(FAKE_APP_NAME); } @Test public void launchFragment_shouldShowSaveButton() { + setUpOneNetworkBundle(); + setupFragment(); + assertThat(mAddAppNetworksFragment.mSaveButton).isNotNull(); } @Test public void launchFragment_shouldShowCancelButton() { + setUpOneNetworkBundle(); + setupFragment(); + assertThat(mAddAppNetworksFragment.mCancelButton).isNotNull(); } + + @Test + public void requestOneNetwork_shouldShowCorrectSSID() { + setUpOneNetworkBundle(); + setupFragment(); + TextView ssidView = (TextView) mAddAppNetworksFragment.mLayoutView.findViewById( + R.id.single_ssid); + + assertThat(ssidView.getText()).isEqualTo(FAKE_SSID); + } + + @Test + public void withNoExtra_requestNetwork_shouldFinished() { + setUpNoNetworkBundle(); + setupFragment(); + + assertThat(mAddAppNetworksFragment.mActivity.isFinishing()).isTrue(); + } + + private void setUpOneNetworkBundle() { + // Setup one network. + List wifiConfigurationList = new ArrayList<>(); + wifiConfigurationList.add( + generateWifiConfig(FAKE_SSID, WifiConfiguration.KeyMgmt.WPA_PSK, "\"1234567890\"")); + // Set up bundle. + final Bundle bundle = new Bundle(); + bundle.putString(AddAppNetworksActivity.KEY_CALLING_PACKAGE_NAME, FAKE_APP_NAME); + bundle.putParcelableArrayList(Settings.EXTRA_WIFI_CONFIGURATION_LIST, + (ArrayList) wifiConfigurationList); + doReturn(bundle).when(mAddAppNetworksFragment).getArguments(); + } + + private void setUpNoNetworkBundle() { + // Set up bundle. + final Bundle bundle = new Bundle(); + bundle.putString(AddAppNetworksActivity.KEY_CALLING_PACKAGE_NAME, FAKE_APP_NAME); + bundle.putParcelableArrayList(Settings.EXTRA_WIFI_CONFIGURATION_LIST, null); + doReturn(bundle).when(mAddAppNetworksFragment).getArguments(); + } + + private void setupFragment() { + FragmentController.setupFragment(mAddAppNetworksFragment); + } + + private static WifiConfiguration generateWifiConfig(String ssid, int securityType, + String password) { + final WifiConfiguration config = new WifiConfiguration(); + config.SSID = ssid; + config.allowedKeyManagement.set(securityType); + + if (password != null) { + config.preSharedKey = password; + } + return config; + } + }