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