From 714cd06b02aea111ed82218531d6a7b2536d6598 Mon Sep 17 00:00:00 2001 From: Quang Luong Date: Fri, 2 Aug 2019 11:14:39 -0700 Subject: [PATCH] Added WifiEntryPreference as WifiEntry version of AccessPointPreference Added WifiEntryPreference and LongPressWifiEntryPreference to replace AccessPointPreference using WifiEntry instead of AccessPoint. Test: atest WifiEntryPreferenceTest && manual - visual verification with dummy entries. Bug: 70983952 Change-Id: I7234a9371737c32ee8d585c2802e4ce03a36a441 --- packages/SettingsLib/Android.bp | 1 + .../wifi/LongPressWifiEntryPreference.java | 46 ++++ .../settingslib/wifi/WifiEntryPreference.java | 238 ++++++++++++++++++ .../wifi/WifiEntryPreferenceTest.java | 150 +++++++++++ 4 files changed, 435 insertions(+) create mode 100644 packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java create mode 100644 packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java create mode 100644 packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 7760e0ef7246c..fea1831b4c5f8 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -24,6 +24,7 @@ android_library { "SettingsLibProgressBar", "SettingsLibAdaptiveIcon", "SettingsLibRadioButtonPreference", + "WifiTrackerLib", ], // ANDROIDMK TRANSLATION ERROR: unsupported assignment to LOCAL_SHARED_JAVA_LIBRARIES diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java new file mode 100644 index 0000000000000..503d60c87bb93 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/LongPressWifiEntryPreference.java @@ -0,0 +1,46 @@ +/* + * 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.settingslib.wifi; + +import android.content.Context; + +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceViewHolder; + +import com.android.wifitrackerlib.WifiEntry; + +/** + * WifiEntryPreference that can be long pressed. + */ +public class LongPressWifiEntryPreference extends WifiEntryPreference { + + private final Fragment mFragment; + + public LongPressWifiEntryPreference(Context context, WifiEntry wifiEntry, Fragment fragment) { + super(context, wifiEntry); + mFragment = fragment; + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder view) { + super.onBindViewHolder(view); + if (mFragment != null) { + view.itemView.setOnCreateContextMenuListener(mFragment); + view.itemView.setTag(this); + view.itemView.setLongClickable(true); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java new file mode 100644 index 0000000000000..22f47f1c65850 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java @@ -0,0 +1,238 @@ +/* + * 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.settingslib.wifi; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.text.TextUtils; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +import com.android.settingslib.R; +import com.android.settingslib.Utils; +import com.android.wifitrackerlib.WifiEntry; + +/** + * Preference to display a WifiEntry in a wifi picker. + */ +public class WifiEntryPreference extends Preference implements WifiEntry.WifiEntryCallback { + + private static final int[] STATE_SECURED = { + R.attr.state_encrypted + }; + + private static final int[] STATE_METERED = { + R.attr.state_metered + }; + + private static final int[] FRICTION_ATTRS = { + R.attr.wifi_friction + }; + + // These values must be kept within [WifiEntry.WIFI_LEVEL_MIN, WifiEntry.WIFI_LEVEL_MAX] + private static final int[] WIFI_CONNECTION_STRENGTH = { + R.string.accessibility_no_wifi, + R.string.accessibility_wifi_one_bar, + R.string.accessibility_wifi_two_bars, + R.string.accessibility_wifi_three_bars, + R.string.accessibility_wifi_signal_full + }; + + // StateListDrawable to display secured lock / metered "$" icon + @Nullable private final StateListDrawable mFrictionSld; + private final IconInjector mIconInjector; + private WifiEntry mWifiEntry; + private int mLevel = -1; + private CharSequence mContentDescription; + + public WifiEntryPreference(@NonNull Context context, @NonNull WifiEntry wifiEntry) { + this(context, wifiEntry, new IconInjector(context)); + } + + @VisibleForTesting + WifiEntryPreference(@NonNull Context context, @NonNull WifiEntry wifiEntry, + @NonNull IconInjector iconInjector) { + super(context); + + setLayoutResource(R.layout.preference_access_point); + setWidgetLayoutResource(R.layout.access_point_friction_widget); + mFrictionSld = getFrictionStateListDrawable(); + mWifiEntry = wifiEntry; + mWifiEntry.setListener(this); + mIconInjector = iconInjector; + refresh(); + } + + public WifiEntry getWifiEntry() { + return mWifiEntry; + } + + @Override + public void onBindViewHolder(final PreferenceViewHolder view) { + super.onBindViewHolder(view); + final Drawable drawable = getIcon(); + if (drawable != null) { + drawable.setLevel(mLevel); + } + + view.itemView.setContentDescription(mContentDescription); + + final ImageView frictionImageView = (ImageView) view.findViewById(R.id.friction_icon); + bindFrictionImage(frictionImageView); + + // Turn off divider + view.findViewById(R.id.two_target_divider).setVisibility(View.INVISIBLE); + } + + /** + * Updates the title and summary; may indirectly call notifyChanged(). + */ + public void refresh() { + setTitle(mWifiEntry.getTitle()); + final int level = mWifiEntry.getLevel(); + if (level != mLevel) { + mLevel = level; + updateIcon(mLevel); + notifyChanged(); + } + + setSummary(mWifiEntry.getSummary()); + mContentDescription = buildContentDescription(); + } + + /** + * Indicates the state of the WifiEntry has changed and clients may retrieve updates through + * the WifiEntry getter methods. + */ + public void onUpdated() { + // TODO(b/70983952): Fill this method in + refresh(); + } + + /** + * Result of the connect request indicated by the WifiEntry.CONNECT_STATUS constants. + */ + public void onConnectResult(int status) { + // TODO(b/70983952): Fill this method in + } + + /** + * Result of the disconnect request indicated by the WifiEntry.DISCONNECT_STATUS constants. + */ + public void onDisconnectResult(int status) { + // TODO(b/70983952): Fill this method in + } + + /** + * Result of the forget request indicated by the WifiEntry.FORGET_STATUS constants. + */ + public void onForgetResult(int status) { + // TODO(b/70983952): Fill this method in + } + + private void updateIcon(int level) { + if (level == -1) { + setIcon(null); + return; + } + + final Drawable drawable = mIconInjector.getIcon(level); + if (drawable != null) { + drawable.setTintList(Utils.getColorAttr(getContext(), + android.R.attr.colorControlNormal)); + setIcon(drawable); + } else { + setIcon(null); + } + } + + @Nullable + private StateListDrawable getFrictionStateListDrawable() { + TypedArray frictionSld; + try { + frictionSld = getContext().getTheme().obtainStyledAttributes(FRICTION_ATTRS); + } catch (Resources.NotFoundException e) { + // Fallback for platforms that do not need friction icon resources. + frictionSld = null; + } + return frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null; + } + + /** + * Binds the friction icon drawable using a StateListDrawable. + * + *

Friction icons will be rebound when notifyChange() is called, and therefore + * do not need to be managed in refresh()

. + */ + private void bindFrictionImage(ImageView frictionImageView) { + if (frictionImageView == null || mFrictionSld == null) { + return; + } + if ((mWifiEntry.getSecurity() != WifiEntry.SECURITY_NONE) + && (mWifiEntry.getSecurity() != WifiEntry.SECURITY_OWE) + && (mWifiEntry.getSecurity() != WifiEntry.SECURITY_OWE_TRANSITION)) { + mFrictionSld.setState(STATE_SECURED); + } else if (mWifiEntry.isMetered()) { + mFrictionSld.setState(STATE_METERED); + } + frictionImageView.setImageDrawable(mFrictionSld.getCurrent()); + } + + /** + * Helper method to generate content description string. + */ + @VisibleForTesting + CharSequence buildContentDescription() { + final Context context = getContext(); + + CharSequence contentDescription = getTitle(); + final CharSequence summary = getSummary(); + if (!TextUtils.isEmpty(summary)) { + contentDescription = TextUtils.concat(contentDescription, ",", summary); + } + int level = mWifiEntry.getLevel(); + if (level >= 0 && level < WIFI_CONNECTION_STRENGTH.length) { + contentDescription = TextUtils.concat(contentDescription, ",", + context.getString(WIFI_CONNECTION_STRENGTH[level])); + } + return TextUtils.concat(contentDescription, ",", + mWifiEntry.getSecurity() == WifiEntry.SECURITY_NONE + ? context.getString(R.string.accessibility_wifi_security_type_none) + : context.getString(R.string.accessibility_wifi_security_type_secured)); + } + + + static class IconInjector { + private final Context mContext; + + IconInjector(Context context) { + mContext = context; + } + + public Drawable getIcon(int level) { + return mContext.getDrawable(Utils.getWifiIconResource(level)); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java new file mode 100644 index 0000000000000..752a5498a077a --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiEntryPreferenceTest.java @@ -0,0 +1,150 @@ +/* + * 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.settingslib.wifi; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import com.android.wifitrackerlib.WifiEntry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class WifiEntryPreferenceTest { + + private Context mContext; + + @Mock + private WifiEntry mMockWifiEntry; + @Mock + private WifiEntryPreference.IconInjector mMockIconInjector; + + @Mock + private Drawable mMockDrawable0; + @Mock + private Drawable mMockDrawable1; + @Mock + private Drawable mMockDrawable2; + @Mock + private Drawable mMockDrawable3; + @Mock + private Drawable mMockDrawable4; + + private static final String MOCK_TITLE = "title"; + private static final String MOCK_SUMMARY = "summary"; + + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + + MockitoAnnotations.initMocks(this); + + when(mMockWifiEntry.getTitle()).thenReturn(MOCK_TITLE); + when(mMockWifiEntry.getSummary()).thenReturn(MOCK_SUMMARY); + + when(mMockIconInjector.getIcon(0)).thenReturn(mMockDrawable0); + when(mMockIconInjector.getIcon(1)).thenReturn(mMockDrawable1); + when(mMockIconInjector.getIcon(2)).thenReturn(mMockDrawable2); + when(mMockIconInjector.getIcon(3)).thenReturn(mMockDrawable3); + when(mMockIconInjector.getIcon(4)).thenReturn(mMockDrawable4); + } + + @Test + public void constructor_shouldSetWifiEntryTitleAndSummary() { + final WifiEntryPreference pref = + new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector); + + assertThat(pref.getTitle()).isEqualTo(MOCK_TITLE); + assertThat(pref.getSummary()).isEqualTo(MOCK_SUMMARY); + } + + @Test + public void constructor_shouldSetIcon() { + when(mMockWifiEntry.getLevel()).thenReturn(0); + + final WifiEntryPreference pref = + new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector); + + assertThat(pref.getIcon()).isEqualTo(mMockDrawable0); + } + + @Test + public void titleChanged_refresh_shouldUpdateTitle() { + final WifiEntryPreference pref = + new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector); + final String updatedTitle = "updated title"; + when(mMockWifiEntry.getTitle()).thenReturn(updatedTitle); + + pref.refresh(); + + assertThat(pref.getTitle()).isEqualTo(updatedTitle); + } + + @Test + public void summaryChanged_refresh_shouldUpdateSummary() { + final WifiEntryPreference pref = + new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector); + final String updatedSummary = "updated summary"; + when(mMockWifiEntry.getSummary()).thenReturn(updatedSummary); + + pref.refresh(); + + assertThat(pref.getSummary()).isEqualTo(updatedSummary); + } + + @Test + public void levelChanged_refresh_shouldUpdateLevelIcon() { + final List iconList = new ArrayList<>(); + final WifiEntryPreference pref = + new WifiEntryPreference(mContext, mMockWifiEntry, mMockIconInjector); + + when(mMockWifiEntry.getLevel()).thenReturn(0); + pref.refresh(); + iconList.add(pref.getIcon()); + when(mMockWifiEntry.getLevel()).thenReturn(1); + pref.refresh(); + iconList.add(pref.getIcon()); + when(mMockWifiEntry.getLevel()).thenReturn(2); + pref.refresh(); + iconList.add(pref.getIcon()); + when(mMockWifiEntry.getLevel()).thenReturn(3); + pref.refresh(); + iconList.add(pref.getIcon()); + when(mMockWifiEntry.getLevel()).thenReturn(4); + pref.refresh(); + iconList.add(pref.getIcon()); + when(mMockWifiEntry.getLevel()).thenReturn(-1); + pref.refresh(); + iconList.add(pref.getIcon()); + + assertThat(iconList).containsExactly(mMockDrawable0, mMockDrawable1, + mMockDrawable2, mMockDrawable3, mMockDrawable4, null); + } +}