1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.wifi.slice; 18 19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; 20 import static android.provider.SettingsSlicesContract.KEY_WIFI; 21 22 import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI; 23 24 import android.annotation.ColorInt; 25 import android.app.PendingIntent; 26 import android.app.settings.SettingsEnums; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.graphics.Color; 30 import android.graphics.PorterDuff; 31 import android.graphics.PorterDuffColorFilter; 32 import android.graphics.drawable.ColorDrawable; 33 import android.graphics.drawable.Drawable; 34 import android.net.ConnectivityManager; 35 import android.net.NetworkCapabilities; 36 import android.net.NetworkInfo; 37 import android.net.Uri; 38 import android.net.wifi.WifiManager; 39 import android.os.Bundle; 40 import android.text.TextUtils; 41 42 import androidx.annotation.VisibleForTesting; 43 import androidx.core.graphics.drawable.IconCompat; 44 import androidx.slice.Slice; 45 import androidx.slice.builders.ListBuilder; 46 import androidx.slice.builders.SliceAction; 47 48 import com.android.settings.R; 49 import com.android.settings.SubSettings; 50 import com.android.settings.Utils; 51 import com.android.settings.core.SubSettingLauncher; 52 import com.android.settings.slices.CustomSliceable; 53 import com.android.settings.slices.SliceBackgroundWorker; 54 import com.android.settings.slices.SliceBuilderUtils; 55 import com.android.settings.wifi.WifiDialogActivity; 56 import com.android.settings.wifi.WifiSettings; 57 import com.android.settings.wifi.WifiUtils; 58 import com.android.settings.wifi.details.WifiNetworkDetailsFragment; 59 import com.android.settingslib.wifi.AccessPoint; 60 61 import java.util.Arrays; 62 import java.util.List; 63 import java.util.Set; 64 import java.util.stream.Collectors; 65 66 /** 67 * {@link CustomSliceable} for Wi-Fi, used by generic clients. 68 */ 69 public class WifiSlice implements CustomSliceable { 70 71 @VisibleForTesting 72 static final int DEFAULT_EXPANDED_ROW_COUNT = 3; 73 74 protected final Context mContext; 75 protected final WifiManager mWifiManager; 76 protected final ConnectivityManager mConnectivityManager; 77 WifiSlice(Context context)78 public WifiSlice(Context context) { 79 mContext = context; 80 mWifiManager = mContext.getSystemService(WifiManager.class); 81 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); 82 } 83 84 @Override getUri()85 public Uri getUri() { 86 return WIFI_SLICE_URI; 87 } 88 89 @Override getSlice()90 public Slice getSlice() { 91 // Reload theme for switching dark mode on/off 92 mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */); 93 94 final boolean isWifiEnabled = isWifiEnabled(); 95 ListBuilder listBuilder = getHeaderRow(isWifiEnabled); 96 if (!isWifiEnabled) { 97 WifiScanWorker.clearClickedWifi(); 98 return listBuilder.build(); 99 } 100 101 final WifiScanWorker worker = SliceBackgroundWorker.getInstance(getUri()); 102 final List<AccessPoint> apList = worker != null ? worker.getResults() : null; 103 final int apCount = apList == null ? 0 : apList.size(); 104 final boolean isFirstApActive = apCount > 0 && apList.get(0).isActive(); 105 handleNetworkCallback(worker, isFirstApActive); 106 107 // Need a loading text when results are not ready or out of date. 108 boolean needLoadingRow = true; 109 // Skip checking the existence of the first access point if it's active 110 int index = isFirstApActive ? 1 : 0; 111 // This loop checks the existence of reachable APs to determine the validity of the current 112 // AP list. 113 for (; index < apCount; index++) { 114 if (apList.get(index).isReachable()) { 115 needLoadingRow = false; 116 break; 117 } 118 } 119 120 // Add AP rows 121 final CharSequence placeholder = mContext.getText(R.string.summary_placeholder); 122 for (int i = 0; i < DEFAULT_EXPANDED_ROW_COUNT; i++) { 123 if (i < apCount) { 124 listBuilder.addRow(getAccessPointRow(apList.get(i))); 125 } else if (needLoadingRow) { 126 listBuilder.addRow(getLoadingRow(placeholder)); 127 needLoadingRow = false; 128 } else { 129 listBuilder.addRow(new ListBuilder.RowBuilder() 130 .setTitle(placeholder) 131 .setSubtitle(placeholder)); 132 } 133 } 134 return listBuilder.build(); 135 } 136 handleNetworkCallback(WifiScanWorker worker, boolean isFirstApActive)137 private void handleNetworkCallback(WifiScanWorker worker, boolean isFirstApActive) { 138 if (worker == null) { 139 return; 140 } 141 if (isFirstApActive) { 142 worker.registerNetworkCallback(mWifiManager.getCurrentNetwork()); 143 } else { 144 worker.unregisterNetworkCallback(); 145 } 146 } 147 getHeaderRow(boolean isWifiEnabled)148 private ListBuilder getHeaderRow(boolean isWifiEnabled) { 149 final IconCompat icon = IconCompat.createWithResource(mContext, 150 R.drawable.ic_settings_wireless); 151 final String title = mContext.getString(R.string.wifi_settings); 152 final PendingIntent toggleAction = getBroadcastIntent(mContext); 153 final PendingIntent primaryAction = getPrimaryAction(); 154 final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, 155 ListBuilder.ICON_IMAGE, title); 156 final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, 157 null /* actionTitle */, isWifiEnabled); 158 159 return new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) 160 .setAccentColor(COLOR_NOT_TINTED) 161 .setKeywords(getKeywords()) 162 .addRow(new ListBuilder.RowBuilder() 163 .setTitle(title) 164 .addEndItem(toggleSliceAction) 165 .setPrimaryAction(primarySliceAction)); 166 } 167 getAccessPointRow(AccessPoint accessPoint)168 private ListBuilder.RowBuilder getAccessPointRow(AccessPoint accessPoint) { 169 final boolean isCaptivePortal = accessPoint.isActive() && isCaptivePortal(); 170 final CharSequence title = accessPoint.getTitle(); 171 final CharSequence summary = getAccessPointSummary(accessPoint, isCaptivePortal); 172 final IconCompat levelIcon = getAccessPointLevelIcon(accessPoint); 173 final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() 174 .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE) 175 .setTitle(title) 176 .setSubtitle(summary) 177 .setPrimaryAction(getAccessPointAction(accessPoint, isCaptivePortal, levelIcon, 178 title)); 179 180 if (isCaptivePortal) { 181 rowBuilder.addEndItem(getCaptivePortalEndAction(accessPoint, title)); 182 } else { 183 final IconCompat endIcon = getEndIcon(accessPoint); 184 if (endIcon != null) { 185 rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE); 186 } 187 } 188 return rowBuilder; 189 } 190 getAccessPointSummary(AccessPoint accessPoint, boolean isCaptivePortal)191 private CharSequence getAccessPointSummary(AccessPoint accessPoint, boolean isCaptivePortal) { 192 if (isCaptivePortal) { 193 return mContext.getText(R.string.wifi_tap_to_sign_in); 194 } 195 196 final CharSequence summary = accessPoint.getSettingsSummary(); 197 return TextUtils.isEmpty(summary) ? mContext.getText(R.string.disconnected) : summary; 198 } 199 getAccessPointLevelIcon(AccessPoint accessPoint)200 private IconCompat getAccessPointLevelIcon(AccessPoint accessPoint) { 201 final Drawable d = mContext.getDrawable( 202 com.android.settingslib.Utils.getWifiIconResource(accessPoint.getLevel())); 203 204 final @ColorInt int color; 205 if (accessPoint.isActive()) { 206 final NetworkInfo.State state = accessPoint.getNetworkInfo().getState(); 207 if (state == NetworkInfo.State.CONNECTED) { 208 color = Utils.getColorAccentDefaultColor(mContext); 209 } else { // connecting 210 color = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext, 211 android.R.attr.colorControlNormal)); 212 } 213 } else { 214 color = Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal); 215 } 216 217 d.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); 218 return Utils.createIconWithDrawable(d); 219 } 220 getEndIcon(AccessPoint accessPoint)221 private IconCompat getEndIcon(AccessPoint accessPoint) { 222 if (accessPoint.isActive()) { 223 return null; 224 } else if (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) { 225 return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed); 226 } else if (accessPoint.isMetered()) { 227 return IconCompat.createWithResource(mContext, R.drawable.ic_friction_money); 228 } 229 return null; 230 } 231 getCaptivePortalEndAction(AccessPoint accessPoint, CharSequence title)232 private SliceAction getCaptivePortalEndAction(AccessPoint accessPoint, CharSequence title) { 233 return getAccessPointAction(accessPoint, false /* isCaptivePortal */, 234 IconCompat.createWithResource(mContext, R.drawable.ic_settings_accent), title); 235 } 236 getAccessPointAction(AccessPoint accessPoint, boolean isCaptivePortal, IconCompat icon, CharSequence title)237 private SliceAction getAccessPointAction(AccessPoint accessPoint, boolean isCaptivePortal, 238 IconCompat icon, CharSequence title) { 239 final int requestCode = accessPoint.hashCode(); 240 if (isCaptivePortal) { 241 final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) 242 .putExtra(ConnectivityManager.EXTRA_NETWORK, mWifiManager.getCurrentNetwork()); 243 return getBroadcastAction(requestCode, intent, icon, title); 244 } 245 246 final Bundle extras = new Bundle(); 247 accessPoint.saveWifiState(extras); 248 249 if (accessPoint.isActive()) { 250 final Intent intent = new SubSettingLauncher(mContext) 251 .setTitleRes(R.string.pref_title_network_details) 252 .setDestination(WifiNetworkDetailsFragment.class.getName()) 253 .setArguments(extras) 254 .setSourceMetricsCategory(SettingsEnums.WIFI) 255 .toIntent(); 256 return getActivityAction(requestCode, intent, icon, title); 257 } else if (WifiUtils.getConnectingType(accessPoint) != WifiUtils.CONNECT_TYPE_OTHERS) { 258 final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) 259 .putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras); 260 return getBroadcastAction(requestCode, intent, icon, title); 261 } else { 262 final Intent intent = new Intent(mContext, WifiDialogActivity.class) 263 .putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras); 264 return getActivityAction(requestCode, intent, icon, title); 265 } 266 } 267 getActivityAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)268 private SliceAction getActivityAction(int requestCode, Intent intent, IconCompat icon, 269 CharSequence title) { 270 final PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent, 271 0 /* flags */); 272 return SliceAction.createDeeplink(pi, icon, ListBuilder.ICON_IMAGE, title); 273 } 274 getBroadcastAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)275 private SliceAction getBroadcastAction(int requestCode, Intent intent, IconCompat icon, 276 CharSequence title) { 277 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 278 final PendingIntent pi = PendingIntent.getBroadcast(mContext, requestCode, intent, 279 PendingIntent.FLAG_UPDATE_CURRENT); 280 return SliceAction.create(pi, icon, ListBuilder.ICON_IMAGE, title); 281 } 282 getLoadingRow(CharSequence placeholder)283 private ListBuilder.RowBuilder getLoadingRow(CharSequence placeholder) { 284 final CharSequence title = mContext.getText(R.string.wifi_empty_list_wifi_on); 285 286 // for aligning to the Wi-Fi AP's name 287 final IconCompat emptyIcon = Utils.createIconWithDrawable( 288 new ColorDrawable(Color.TRANSPARENT)); 289 290 return new ListBuilder.RowBuilder() 291 .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE) 292 .setTitle(placeholder) 293 .setSubtitle(title); 294 } 295 isCaptivePortal()296 private boolean isCaptivePortal() { 297 final NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities( 298 mWifiManager.getCurrentNetwork()); 299 return WifiUtils.canSignIntoNetwork(nc); 300 } 301 302 /** 303 * Update the current wifi status to the boolean value keyed by 304 * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}. 305 */ 306 @Override onNotifyChange(Intent intent)307 public void onNotifyChange(Intent intent) { 308 final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, 309 mWifiManager.isWifiEnabled()); 310 mWifiManager.setWifiEnabled(newState); 311 // Do not notifyChange on Uri. The service takes longer to update the current value than it 312 // does for the Slice to check the current value again. Let {@link WifiScanWorker} 313 // handle it. 314 } 315 316 @Override getIntent()317 public Intent getIntent() { 318 final String screenTitle = mContext.getText(R.string.wifi_settings).toString(); 319 final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build(); 320 final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, 321 WifiSettings.class.getName(), KEY_WIFI, screenTitle, 322 SettingsEnums.DIALOG_WIFI_AP_EDIT) 323 .setClassName(mContext.getPackageName(), SubSettings.class.getName()) 324 .setData(contentUri); 325 326 return intent; 327 } 328 isWifiEnabled()329 private boolean isWifiEnabled() { 330 switch (mWifiManager.getWifiState()) { 331 case WifiManager.WIFI_STATE_ENABLED: 332 case WifiManager.WIFI_STATE_ENABLING: 333 return true; 334 default: 335 return false; 336 } 337 } 338 getPrimaryAction()339 private PendingIntent getPrimaryAction() { 340 final Intent intent = getIntent(); 341 return PendingIntent.getActivity(mContext, 0 /* requestCode */, 342 intent, 0 /* flags */); 343 } 344 getKeywords()345 private Set<String> getKeywords() { 346 final String keywords = mContext.getString(R.string.keywords_wifi); 347 return Arrays.asList(TextUtils.split(keywords, ",")) 348 .stream() 349 .map(String::trim) 350 .collect(Collectors.toSet()); 351 } 352 353 @Override getBackgroundWorkerClass()354 public Class getBackgroundWorkerClass() { 355 return WifiScanWorker.class; 356 } 357 } 358