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