diff --git a/res/values/strings.xml b/res/values/strings.xml index 1e1304e893f..10bd7246ffb 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3013,6 +3013,10 @@ Use %1$s instead of %2$s as your SMS app? Use %s as your SMS app? + + Network Scorer + None + Change Wi\u2011Fi assistant? diff --git a/res/xml/network_scorer_picker_prefs.xml b/res/xml/network_scorer_picker_prefs.xml new file mode 100644 index 00000000000..fab69e76f87 --- /dev/null +++ b/res/xml/network_scorer_picker_prefs.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/res/xml/wifi_configure_settings.xml b/res/xml/wifi_configure_settings.xml index bf1dc7586ea..8e51009c6fd 100644 --- a/res/xml/wifi_configure_settings.xml +++ b/res/xml/wifi_configure_settings.xml @@ -58,6 +58,11 @@ android:fragment="com.android.settings.WifiCallingSettings" settings:keywords="@string/keywords_wifi_calling"/> + + diff --git a/src/com/android/settings/network/NetworkScoreManagerWrapper.java b/src/com/android/settings/network/NetworkScoreManagerWrapper.java new file mode 100644 index 00000000000..0d353781797 --- /dev/null +++ b/src/com/android/settings/network/NetworkScoreManagerWrapper.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 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.network; + +import android.annotation.Nullable; +import android.net.NetworkScoreManager; +import android.net.NetworkScorerAppData; + +import java.util.List; + +/** + * Wrapper around {@link NetworkScoreManager} to facilitate unit testing. + * + * TODO: delete this class once robolectric supports Android O + */ +public class NetworkScoreManagerWrapper { + private final NetworkScoreManager mNetworkScoreManager; + + public NetworkScoreManagerWrapper(NetworkScoreManager networkScoreManager) { + mNetworkScoreManager = networkScoreManager; + } + + /** + * Returns the list of available scorer apps. The list will be empty if there are + * no valid scorers. + */ + public List getAllValidScorers() { + return mNetworkScoreManager.getAllValidScorers(); + } + + /** + * Obtain the package name of the current active network scorer. + * + *

At any time, only one scorer application will receive {@link #ACTION_SCORE_NETWORKS} + * broadcasts and be allowed to call {@link #updateScores}. Applications may use this method to + * determine the current scorer and offer the user the ability to select a different scorer via + * the {@link #ACTION_CHANGE_ACTIVE} intent. + * @return the full package name of the current active scorer, or null if there is no active + * scorer. + */ + @Nullable + public String getActiveScorerPackage() { + return mNetworkScoreManager.getActiveScorerPackage(); + } + + + /** + * Set the active scorer to a new package and clear existing scores. + * + *

Should never be called directly without obtaining user consent. This can be done by using + * the {@link #ACTION_CHANGE_ACTIVE} broadcast, or using a custom configuration activity. + * + * @return true if the operation succeeded, or false if the new package is not a valid scorer. + * @throws SecurityException if the caller is not a system process or does not hold the + * {@link android.Manifest.permission#REQUEST_NETWORK_SCORES} permission + */ + public boolean setActiveScorer(String packageName) throws SecurityException { + return mNetworkScoreManager.setActiveScorer(packageName); + } +} \ No newline at end of file diff --git a/src/com/android/settings/network/NetworkScorerPicker.java b/src/com/android/settings/network/NetworkScorerPicker.java new file mode 100644 index 00000000000..da9d84f6a66 --- /dev/null +++ b/src/com/android/settings/network/NetworkScorerPicker.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2017 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.network; + +import android.content.Context; +import android.net.NetworkScoreManager; +import android.net.NetworkScorerAppData; +import android.os.Bundle; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.settings.R; +import com.android.settings.core.InstrumentedPreferenceFragment; +import com.android.settings.core.instrumentation.Instrumentable; +import com.android.settings.widget.RadioButtonPreference; + +import java.util.List; + +/** + * Fragment for choosing default network scorer. + */ +public class NetworkScorerPicker extends InstrumentedPreferenceFragment implements + RadioButtonPreference.OnClickListener { + + private NetworkScoreManagerWrapper mNetworkScoreManager; + + @Override + public int getMetricsCategory() { + //TODO(35854268): Add logging. + return Instrumentable.METRICS_CATEGORY_UNKNOWN; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + addPreferencesFromResource(R.xml.network_scorer_picker_prefs); + updateCandidates(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mNetworkScoreManager = createNetworkScorerManagerWrapper(context); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + // this is needed so the back button goes back to previous fragment + setHasOptionsMenu(true); + return view; + } + + @VisibleForTesting + public void updateCandidates() { + final PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + + final List scorers = mNetworkScoreManager.getAllValidScorers(); + if (scorers.isEmpty()) { + final RadioButtonPreference nonePref = new RadioButtonPreference(getPrefContext()); + nonePref.setTitle(R.string.network_scorer_picker_none_preference); + nonePref.setChecked(true); + screen.addPreference(nonePref); + return; + } + final String defaultAppKey = getActiveScorerPackage(); + final int numScorers = scorers.size(); + for (int i = 0; i < numScorers; i++) { + final RadioButtonPreference pref = new RadioButtonPreference(getPrefContext()); + final NetworkScorerAppData appData = scorers.get(i); + final String appKey = appData.getRecommendationServicePackageName(); + pref.setTitle(appData.getRecommendationServiceLabel()); + pref.setKey(appKey); + pref.setChecked(TextUtils.equals(defaultAppKey, appKey)); + pref.setOnClickListener(this); + screen.addPreference(pref); + } + } + + private String getActiveScorerPackage() { + return mNetworkScoreManager.getActiveScorerPackage(); + } + + private boolean setActiveScorer(String key) { + if (!TextUtils.equals(key, getActiveScorerPackage())) { + return mNetworkScoreManager.setActiveScorer(key); + } + return false; + } + + @Override + public void onRadioButtonClicked(RadioButtonPreference selected) { + final String selectedKey = selected.getKey(); + final boolean success = setActiveScorer(selectedKey); + if (success) { + updateCheckedState(selectedKey); + } + } + + private void updateCheckedState(String selectedKey) { + final PreferenceScreen screen = getPreferenceScreen(); + final int count = screen.getPreferenceCount(); + for (int i = 0; i < count; i++) { + final Preference pref = screen.getPreference(i); + if (pref instanceof RadioButtonPreference) { + final RadioButtonPreference radioPref = (RadioButtonPreference) pref; + radioPref.setChecked(TextUtils.equals(pref.getKey(), selectedKey)); + } + } + } + + @VisibleForTesting + NetworkScoreManagerWrapper createNetworkScorerManagerWrapper(Context context) { + return new NetworkScoreManagerWrapper(context.getSystemService(NetworkScoreManager.class)); + } +} diff --git a/tests/robotests/src/android/net/NetworkScorerAppData.java b/tests/robotests/src/android/net/NetworkScorerAppData.java new file mode 100644 index 00000000000..1eaa8a7a7f4 --- /dev/null +++ b/tests/robotests/src/android/net/NetworkScorerAppData.java @@ -0,0 +1,112 @@ +package android.net; + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Holds metadata about a discovered network scorer/recommendation application. + * + * TODO: delete this class once robolectric supports Android O + */ +public final class NetworkScorerAppData implements Parcelable { + /** UID of the scorer app. */ + public final int packageUid; + private final ComponentName mRecommendationService; + /** User visible label in Settings for the recommendation service. */ + private final String mRecommendationServiceLabel; + /** + * The {@link ComponentName} of the Activity to start before enabling the "connect to open + * wifi networks automatically" feature. + */ + private final ComponentName mEnableUseOpenWifiActivity; + + public NetworkScorerAppData(int packageUid, ComponentName recommendationServiceComp, + String recommendationServiceLabel, ComponentName enableUseOpenWifiActivity) { + this.packageUid = packageUid; + this.mRecommendationService = recommendationServiceComp; + this.mRecommendationServiceLabel = recommendationServiceLabel; + this.mEnableUseOpenWifiActivity = enableUseOpenWifiActivity; + } + + protected NetworkScorerAppData(Parcel in) { + packageUid = in.readInt(); + mRecommendationService = ComponentName.readFromParcel(in); + mRecommendationServiceLabel = in.readString(); + mEnableUseOpenWifiActivity = ComponentName.readFromParcel(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(packageUid); + ComponentName.writeToParcel(mRecommendationService, dest); + dest.writeString(mRecommendationServiceLabel); + ComponentName.writeToParcel(mEnableUseOpenWifiActivity, dest); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = + new Creator() { + @Override + public NetworkScorerAppData createFromParcel(Parcel in) { + return new NetworkScorerAppData(in); + } + + @Override + public NetworkScorerAppData[] newArray(int size) { + return new NetworkScorerAppData[size]; + } + }; + + public String getRecommendationServicePackageName() { + return mRecommendationService.getPackageName(); + } + + public ComponentName getRecommendationServiceComponent() { + return mRecommendationService; + } + + @Nullable + public ComponentName getEnableUseOpenWifiActivity() { + return mEnableUseOpenWifiActivity; + } + + @Nullable + public String getRecommendationServiceLabel() { + return mRecommendationServiceLabel; + } + + @Override + public String toString() { + return "NetworkScorerAppData{" + + "packageUid=" + packageUid + + ", mRecommendationService=" + mRecommendationService + + ", mRecommendationServiceLabel=" + mRecommendationServiceLabel + + ", mEnableUseOpenWifiActivity=" + mEnableUseOpenWifiActivity + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NetworkScorerAppData that = (NetworkScorerAppData) o; + return packageUid == that.packageUid && + Objects.equals(mRecommendationService, that.mRecommendationService) && + Objects.equals(mRecommendationServiceLabel, that.mRecommendationServiceLabel) && + Objects.equals(mEnableUseOpenWifiActivity, that.mEnableUseOpenWifiActivity); + } + + @Override + public int hashCode() { + return Objects.hash(packageUid, mRecommendationService, mRecommendationServiceLabel, + mEnableUseOpenWifiActivity); + } +} diff --git a/tests/robotests/src/com/android/settings/network/NetworkScorerPickerTest.java b/tests/robotests/src/com/android/settings/network/NetworkScorerPickerTest.java new file mode 100644 index 00000000000..fef6f851bc9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/NetworkScorerPickerTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2017 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.network; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.net.NetworkScorerAppData; +import android.support.v7.preference.PreferenceManager; +import android.support.v7.preference.PreferenceScreen; +import com.android.settings.R; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.widget.RadioButtonPreference; +import com.google.android.collect.Lists; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; + + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class NetworkScorerPickerTest { + + private static final String TEST_SCORER_PACKAGE_1 = "Test Package 1"; + private static final String TEST_SCORER_CLASS_1 = "Test Class 1"; + private static final String TEST_SCORER_LABEL_1 = "Test Label 1"; + private static final String TEST_SCORER_PACKAGE_2 = "Test Package 2"; + + private Context mContext; + @Mock + private NetworkScoreManagerWrapper mNetworkScoreManager; + @Mock + private PreferenceScreen mPreferenceScreen; + + private TestFragment mFragment; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mFragment = new TestFragment(mContext, mPreferenceScreen, mNetworkScoreManager); + mFragment.onAttach(mContext); + } + + @Test + public void testOnRadioButtonClicked_success() { + RadioButtonPreference pref = new RadioButtonPreference(mContext); + pref.setKey(TEST_SCORER_PACKAGE_1); + when(mPreferenceScreen.getPreference(anyInt())).thenReturn(pref); + when(mPreferenceScreen.getPreferenceCount()).thenReturn(1); + when(mNetworkScoreManager.setActiveScorer(TEST_SCORER_PACKAGE_1)).thenReturn(true); + when(mNetworkScoreManager.getActiveScorerPackage()).thenReturn(TEST_SCORER_PACKAGE_2); + + mFragment.onRadioButtonClicked(pref); + + verify(mNetworkScoreManager).setActiveScorer(TEST_SCORER_PACKAGE_1); + assertThat(pref.isChecked()).isTrue(); + } + + @Test + public void testOnRadioButtonClicked_currentScorer_doNothing() { + RadioButtonPreference pref = new RadioButtonPreference(mContext); + pref.setKey(TEST_SCORER_PACKAGE_1); + pref.setChecked(true); + when(mPreferenceScreen.getPreference(anyInt())).thenReturn(pref); + when(mPreferenceScreen.getPreferenceCount()).thenReturn(1); + when(mNetworkScoreManager.setActiveScorer(TEST_SCORER_PACKAGE_1)).thenReturn(true); + when(mNetworkScoreManager.getActiveScorerPackage()).thenReturn(TEST_SCORER_PACKAGE_1); + + mFragment.onRadioButtonClicked(pref); + + verify(mNetworkScoreManager, never()).setActiveScorer(any()); + assertThat(pref.isChecked()).isTrue(); + } + + @Test + public void testUpdateCandidates_noValidScorers_nonePreference() { + when(mNetworkScoreManager.getAllValidScorers()).thenReturn(new ArrayList<>()); + ArgumentCaptor arg = + ArgumentCaptor.forClass(RadioButtonPreference.class); + + mFragment.updateCandidates(); + + verify(mPreferenceScreen).addPreference(arg.capture()); + assertThat(arg.getValue().getTitle()).isEqualTo( + mContext.getString(R.string.network_scorer_picker_none_preference)); + } + + @Test + public void testUpdateCandidates_validScorer() { + ComponentName scorer = new ComponentName(TEST_SCORER_PACKAGE_1, TEST_SCORER_CLASS_1); + NetworkScorerAppData scorerAppData = new NetworkScorerAppData( + 0, scorer, TEST_SCORER_LABEL_1, null /* enableUseOpenWifiActivity */); + when(mNetworkScoreManager.getAllValidScorers()).thenReturn( + Lists.newArrayList(scorerAppData)); + when(mNetworkScoreManager.getActiveScorerPackage()).thenReturn(TEST_SCORER_PACKAGE_1); + ArgumentCaptor arg = + ArgumentCaptor.forClass(RadioButtonPreference.class); + + mFragment.updateCandidates(); + + verify(mPreferenceScreen).addPreference(arg.capture()); + RadioButtonPreference pref = arg.getValue(); + assertThat(pref.getTitle()).isEqualTo(TEST_SCORER_LABEL_1); + assertThat(pref.isChecked()).isTrue(); + } + + public static class TestFragment extends NetworkScorerPicker { + + private final Context mContext; + private final PreferenceScreen mScreen; + private final PreferenceManager mPrefManager; + private final NetworkScoreManagerWrapper mNetworkScoreManagerWrapper; + + public TestFragment(Context context, PreferenceScreen preferenceScreen, + NetworkScoreManagerWrapper networkScoreManagerWrapper) { + mContext = context; + mScreen = preferenceScreen; + mNetworkScoreManagerWrapper = networkScoreManagerWrapper; + mPrefManager = mock(PreferenceManager.class); + when(mPrefManager.getContext()).thenReturn(context); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public PreferenceManager getPreferenceManager() { + return mPrefManager; + } + + @Override + public PreferenceScreen getPreferenceScreen() { + return mScreen; + } + + @Override + NetworkScoreManagerWrapper createNetworkScorerManagerWrapper(Context context) { + return mNetworkScoreManagerWrapper; + } + } +}