1 package com.android.tv.settings.widget;
2 
3 import android.content.Context;
4 import android.content.pm.PackageManager;
5 import android.content.res.ColorStateList;
6 import android.content.res.Resources;
7 import android.content.res.TypedArray;
8 import android.graphics.drawable.Drawable;
9 import android.graphics.drawable.StateListDrawable;
10 import android.net.wifi.WifiConfiguration;
11 import android.os.Looper;
12 import android.os.UserHandle;
13 import android.text.TextUtils;
14 import android.util.AttributeSet;
15 import android.util.SparseArray;
16 import android.view.View;
17 import android.widget.ImageView;
18 import android.widget.TextView;
19 
20 import androidx.annotation.Nullable;
21 import androidx.annotation.VisibleForTesting;
22 import androidx.preference.Preference;
23 import androidx.preference.PreferenceViewHolder;
24 
25 import com.android.tv.settings.R;
26 import com.android.settingslib.wifi.AccessPoint;
27 import com.android.settingslib.wifi.AccessPoint.Speed;
28 
29 public class AccessPointPreference extends Preference {
30 
31   private static final int[] STATE_SECURED = {
32       R.attr.state_encrypted
33   };
34 
35   private static final int[] STATE_METERED = {
36       R.attr.state_metered
37   };
38 
39   private static final int[] FRICTION_ATTRS = {
40       R.attr.wifi_friction
41   };
42 
43   private static final int[] WIFI_CONNECTION_STRENGTH = {
44       R.string.accessibility_no_wifi,
45       R.string.accessibility_wifi_one_bar,
46       R.string.accessibility_wifi_two_bars,
47       R.string.accessibility_wifi_three_bars,
48       R.string.accessibility_wifi_signal_full
49   };
50 
51   @Nullable
52   private final StateListDrawable mFrictionSld;
53   private final int mBadgePadding;
54   private final UserBadgeCache mBadgeCache;
55   private final IconInjector mIconInjector;
56   private TextView mTitleView;
57   private boolean mShowDivider;
58 
59   private boolean mForSavedNetworks = false;
60   private AccessPoint mAccessPoint;
61   private int mLevel;
62   private CharSequence mContentDescription;
63   private int mDefaultIconResId;
64   private int mWifiSpeed = Speed.NONE;
65 
66   @Nullable
getFrictionStateListDrawable(Context context)67   private static StateListDrawable getFrictionStateListDrawable(Context context) {
68     TypedArray frictionSld;
69     try {
70       frictionSld = context.getTheme().obtainStyledAttributes(FRICTION_ATTRS);
71     } catch (Resources.NotFoundException e) {
72       // Fallback for platforms that do not need friction icon resources.
73       frictionSld = null;
74     }
75     return frictionSld != null ? (StateListDrawable) frictionSld.getDrawable(0) : null;
76   }
77 
78   // Used for fake pref.
AccessPointPreference(Context context, AttributeSet attrs)79   public AccessPointPreference(Context context, AttributeSet attrs) {
80     super(context, attrs);
81     mFrictionSld = null;
82     mBadgePadding = 0;
83     mBadgeCache = null;
84     mIconInjector = new IconInjector(context);
85   }
86 
AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache, boolean forSavedNetworks)87   public AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
88       boolean forSavedNetworks) {
89     this(accessPoint, context, cache, 0 /* iconResId */, forSavedNetworks);
90     refresh();
91   }
92 
AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache, int iconResId, boolean forSavedNetworks)93   public AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
94       int iconResId, boolean forSavedNetworks) {
95     this(accessPoint, context, cache, iconResId, forSavedNetworks,
96         getFrictionStateListDrawable(context), -1 /* level */, new IconInjector(context));
97   }
98 
99   @VisibleForTesting
AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache, int iconResId, boolean forSavedNetworks, StateListDrawable frictionSld, int level, IconInjector iconInjector)100   AccessPointPreference(AccessPoint accessPoint, Context context, UserBadgeCache cache,
101       int iconResId, boolean forSavedNetworks, StateListDrawable frictionSld,
102       int level, IconInjector iconInjector) {
103     super(context);
104     setLayoutResource(R.layout.preference_access_point);
105     setWidgetLayoutResource(getWidgetLayoutResourceId());
106     mBadgeCache = cache;
107     mAccessPoint = accessPoint;
108     mForSavedNetworks = forSavedNetworks;
109     mAccessPoint.setTag(this);
110     mLevel = level;
111     mDefaultIconResId = iconResId;
112     mFrictionSld = frictionSld;
113     mIconInjector = iconInjector;
114     mBadgePadding = context.getResources()
115         .getDimensionPixelSize(R.dimen.wifi_preference_badge_padding);
116   }
117 
getWidgetLayoutResourceId()118   protected int getWidgetLayoutResourceId() {
119     return R.layout.access_point_friction_widget;
120   }
121 
getAccessPoint()122   public AccessPoint getAccessPoint() {
123     return mAccessPoint;
124   }
125 
126   @Override
onBindViewHolder(final PreferenceViewHolder view)127   public void onBindViewHolder(final PreferenceViewHolder view) {
128     super.onBindViewHolder(view);
129     if (mAccessPoint == null) {
130       // Used for fake pref.
131       return;
132     }
133     Drawable drawable = getIcon();
134     if (drawable != null) {
135       drawable.setLevel(mLevel);
136     }
137 
138     mTitleView = (TextView) view.findViewById(android.R.id.title);
139     if (mTitleView != null) {
140       // Attach to the end of the title view
141       mTitleView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null);
142       mTitleView.setCompoundDrawablePadding(mBadgePadding);
143     }
144     view.itemView.setContentDescription(mContentDescription);
145 
146     ImageView frictionImageView = (ImageView) view.findViewById(R.id.friction_icon);
147     bindFrictionImage(frictionImageView);
148 
149     final View divider = view.findViewById(R.id.two_target_divider);
150     divider.setVisibility(shouldShowDivider() ? View.VISIBLE : View.INVISIBLE);
151   }
152 
shouldShowDivider()153   public boolean shouldShowDivider() {
154     return mShowDivider;
155   }
156 
setShowDivider(boolean showDivider)157   public void setShowDivider(boolean showDivider) {
158     mShowDivider = showDivider;
159     notifyChanged();
160   }
161 
updateIcon(int level, Context context)162   protected void updateIcon(int level, Context context) {
163     if (level == -1) {
164       safeSetDefaultIcon();
165       return;
166     }
167 
168     Drawable drawable = mIconInjector.getIcon(level);
169     if (!mForSavedNetworks && drawable != null) {
170       drawable.setTintList(getColorAttr(context, android.R.attr.colorControlNormal));
171       setIcon(drawable);
172     } else {
173       safeSetDefaultIcon();
174     }
175   }
176 
177   /**
178    * Binds the friction icon drawable using a StateListDrawable.
179    *
180    * <p>Friction icons will be rebound when notifyChange() is called, and therefore
181    * do not need to be managed in refresh()</p>.
182    */
bindFrictionImage(ImageView frictionImageView)183   private void bindFrictionImage(ImageView frictionImageView) {
184     if (frictionImageView == null || mFrictionSld == null) {
185       return;
186     }
187     if ((mAccessPoint.getSecurity() != AccessPoint.SECURITY_NONE)
188         && (mAccessPoint.getSecurity() != AccessPoint.SECURITY_OWE)) {
189       mFrictionSld.setState(STATE_SECURED);
190     } else if (mAccessPoint.isMetered()) {
191       mFrictionSld.setState(STATE_METERED);
192     }
193     Drawable drawable = mFrictionSld.getCurrent();
194     frictionImageView.setImageDrawable(drawable);
195   }
196 
safeSetDefaultIcon()197   private void safeSetDefaultIcon() {
198     if (mDefaultIconResId != 0) {
199       setIcon(mDefaultIconResId);
200     } else {
201       setIcon(null);
202     }
203   }
204 
updateBadge(Context context)205   protected void updateBadge(Context context) {
206     WifiConfiguration config = mAccessPoint.getConfig();
207     if (config != null) {
208       // Fetch badge (may be null)
209       // Get the badge using a cache since the PM will ask the UserManager for the list
210       // of profiles every time otherwise.
211     }
212   }
213 
214   /**
215    * Updates the title and summary; may indirectly call notifyChanged().
216    */
refresh()217   public void refresh() {
218     setTitle(this, mAccessPoint);
219     final Context context = getContext();
220     int level = mAccessPoint.getLevel();
221     int wifiSpeed = Speed.NONE;
222     if (level != mLevel || wifiSpeed != mWifiSpeed) {
223       mLevel = level;
224       mWifiSpeed = wifiSpeed;
225       updateIcon(mLevel, context);
226       notifyChanged();
227     }
228 
229     updateBadge(context);
230 
231     setSummary(mAccessPoint.getSettingsSummary());
232 
233     mContentDescription = buildContentDescription(getContext(), this /* pref */, mAccessPoint);
234   }
235 
236   @Override
notifyChanged()237   protected void notifyChanged() {
238     if (Looper.getMainLooper() != Looper.myLooper()) {
239       // Let our BG thread callbacks call setTitle/setSummary.
240       postNotifyChanged();
241     } else {
242       super.notifyChanged();
243     }
244   }
245 
246   @VisibleForTesting
setTitle(AccessPointPreference preference, AccessPoint ap)247   static void setTitle(AccessPointPreference preference, AccessPoint ap) {
248     preference.setTitle(ap.getTitle());
249   }
250 
251   /**
252    * Helper method to generate content description string.
253    */
254   @VisibleForTesting
buildContentDescription(Context context, Preference pref, AccessPoint ap)255   static CharSequence buildContentDescription(Context context, Preference pref, AccessPoint ap) {
256     CharSequence contentDescription = pref.getTitle();
257     final CharSequence summary = pref.getSummary();
258     if (!TextUtils.isEmpty(summary)) {
259       contentDescription = TextUtils.concat(contentDescription, ",", summary);
260     }
261     int level = ap.getLevel();
262     if (level >= 0 && level < WIFI_CONNECTION_STRENGTH.length) {
263       contentDescription = TextUtils.concat(contentDescription, ",",
264           context.getString(WIFI_CONNECTION_STRENGTH[level]));
265     }
266     return TextUtils.concat(contentDescription, ",",
267         ap.getSecurity() == AccessPoint.SECURITY_NONE
268             ? context.getString(R.string.accessibility_wifi_security_type_none)
269             : context.getString(R.string.accessibility_wifi_security_type_secured));
270   }
271 
onLevelChanged()272   public void onLevelChanged() {
273     postNotifyChanged();
274   }
275 
postNotifyChanged()276   private void postNotifyChanged() {
277     if (mTitleView != null) {
278       mTitleView.post(mNotifyChanged);
279     } // Otherwise we haven't been bound yet, and don't need to update.
280   }
281 
282   private final Runnable mNotifyChanged = new Runnable() {
283     @Override
284     public void run() {
285       notifyChanged();
286     }
287   };
288 
289   public static class UserBadgeCache {
290     private final SparseArray<Drawable> mBadges = new SparseArray<>();
291     private final PackageManager mPm;
292 
UserBadgeCache(PackageManager pm)293     public UserBadgeCache(PackageManager pm) {
294       mPm = pm;
295     }
296   }
297 
298   static class IconInjector {
299     private final Context mContext;
300 
IconInjector(Context context)301     public IconInjector(Context context) {
302       mContext = context;
303     }
304 
getIcon(int level)305     public Drawable getIcon(int level) {
306       return mContext.getDrawable(getWifiIconResource(level));
307     }
308   }
309 
310   static final int[] WIFI_PIE = {
311       R.drawable.ic_wifi_signal_0,
312       R.drawable.ic_wifi_signal_1,
313       R.drawable.ic_wifi_signal_2,
314       R.drawable.ic_wifi_signal_3,
315       R.drawable.ic_wifi_signal_4
316   };
317 
318   static final int[] SHOW_X_WIFI_PIE = {
319       R.drawable.ic_show_x_wifi_signal_0,
320       R.drawable.ic_show_x_wifi_signal_1,
321       R.drawable.ic_show_x_wifi_signal_2,
322       R.drawable.ic_show_x_wifi_signal_3,
323       R.drawable.ic_show_x_wifi_signal_4
324   };
325 
326   /**
327    * Returns the Wifi icon resource for a given RSSI level.
328    *
329    * @param level The number of bars to show (0-4)
330    * @throws IllegalArgumentException if an invalid RSSI level is given.
331    */
getWifiIconResource(int level)332   public static int getWifiIconResource(int level) {
333     return getWifiIconResource(false /* showX */, level);
334   }
335 
336   /**
337    * Returns the Wifi icon resource for a given RSSI level.
338    *
339    * @param showX True if a connected Wi-Fi network has the problem which should show Pie+x
340    *              signal icon to users.
341    * @param level The number of bars to show (0-4)
342    * @throws IllegalArgumentException if an invalid RSSI level is given.
343    */
getWifiIconResource(boolean showX, int level)344   public static int getWifiIconResource(boolean showX, int level) {
345     if (level < 0 || level >= WIFI_PIE.length) {
346       throw new IllegalArgumentException("No Wifi icon found for level: " + level);
347     }
348     return showX ? SHOW_X_WIFI_PIE[level] : WIFI_PIE[level];
349   }
350 
getColorAttr(Context context, int attr)351   public static ColorStateList getColorAttr(Context context, int attr) {
352     TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
353     ColorStateList stateList = null;
354     try {
355       stateList = ta.getColorStateList(0);
356     } finally {
357       ta.recycle();
358     }
359     return stateList;
360   }
361 }
362