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 import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource; 24 25 import android.annotation.ColorInt; 26 import android.app.PendingIntent; 27 import android.app.settings.SettingsEnums; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.graphics.Color; 32 import android.graphics.drawable.ColorDrawable; 33 import android.graphics.drawable.Drawable; 34 import android.net.Uri; 35 import android.net.wifi.WifiManager; 36 import android.os.Binder; 37 import android.os.Bundle; 38 import android.os.UserManager; 39 import android.text.TextUtils; 40 import android.util.EventLog; 41 import android.util.Log; 42 43 import androidx.annotation.Nullable; 44 import androidx.annotation.VisibleForTesting; 45 import androidx.core.graphics.drawable.IconCompat; 46 import androidx.slice.Slice; 47 import androidx.slice.builders.ListBuilder; 48 import androidx.slice.builders.SliceAction; 49 50 import com.android.settings.R; 51 import com.android.settings.SubSettings; 52 import com.android.settings.Utils; 53 import com.android.settings.core.SubSettingLauncher; 54 import com.android.settings.network.NetworkProviderSettings; 55 import com.android.settings.network.SatelliteRepository; 56 import com.android.settings.network.WifiSwitchPreferenceController; 57 import com.android.settings.slices.CustomSliceable; 58 import com.android.settings.slices.SliceBackgroundWorker; 59 import com.android.settings.slices.SliceBuilderUtils; 60 import com.android.settings.wifi.AppStateChangeWifiStateBridge; 61 import com.android.settings.wifi.WifiDialogActivity; 62 import com.android.settings.wifi.WifiUtils; 63 import com.android.settings.wifi.details.WifiNetworkDetailsFragment; 64 import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils; 65 import com.android.wifitrackerlib.WifiEntry; 66 67 import java.util.Arrays; 68 import java.util.List; 69 import java.util.Set; 70 import java.util.concurrent.ExecutionException; 71 import java.util.concurrent.Executors; 72 import java.util.concurrent.TimeUnit; 73 import java.util.concurrent.TimeoutException; 74 import java.util.stream.Collectors; 75 76 /** 77 * {@link CustomSliceable} for Wi-Fi, used by generic clients. 78 * 79 * @deprecated This is not used after V and will be removed. 80 */ 81 @Deprecated(forRemoval = true) 82 public class WifiSlice implements CustomSliceable { 83 84 @VisibleForTesting 85 static final int DEFAULT_EXPANDED_ROW_COUNT = 3; 86 private static final String TAG = "WifiSlice"; 87 88 protected final Context mContext; 89 protected final WifiManager mWifiManager; 90 protected final WifiRestriction mWifiRestriction; 91 WifiSlice(Context context)92 public WifiSlice(Context context) { 93 this(context, new WifiRestriction()); 94 } 95 96 @VisibleForTesting WifiSlice(Context context, WifiRestriction wifiRestriction)97 WifiSlice(Context context, WifiRestriction wifiRestriction) { 98 mContext = context; 99 mWifiManager = mContext.getSystemService(WifiManager.class); 100 mWifiRestriction = wifiRestriction; 101 } 102 103 @Override getUri()104 public Uri getUri() { 105 return WIFI_SLICE_URI; 106 } 107 108 @Override getSlice()109 public Slice getSlice() { 110 final boolean isWifiEnabled = isWifiEnabled(); 111 // If user is a guest just return a slice without a toggle. 112 if (isGuestUser(mContext)) { 113 Log.e(TAG, "Guest user is not allowed to configure Wi-Fi!"); 114 EventLog.writeEvent(0x534e4554, "232798363", -1 /* UID */, "User is a guest"); 115 return getListBuilder(isWifiEnabled, null /* wifiSliceItem */, 116 false /* isWiFiPermissionGranted */).build(); 117 } 118 119 // If external calling package doesn't have Wi-Fi permission. 120 final boolean isPermissionGranted = 121 isCallerExemptUid(mContext) || isPermissionGranted(mContext); 122 ListBuilder listBuilder = getListBuilder(isWifiEnabled, null /* wifiSliceItem */, 123 isPermissionGranted); 124 // If the caller doesn't have the permission granted, just return a slice without a toggle. 125 if (!isWifiEnabled || !isPermissionGranted) { 126 return listBuilder.build(); 127 } 128 129 final WifiScanWorker worker = SliceBackgroundWorker.getInstance(getUri()); 130 final List<WifiSliceItem> apList = worker != null ? worker.getResults() : null; 131 final int apCount = apList == null ? 0 : apList.size(); 132 final boolean isFirstApActive = apCount > 0 133 && apList.get(0).getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED; 134 135 if (isFirstApActive) { 136 // refresh header subtext 137 listBuilder = getListBuilder( 138 true /* isWifiEnabled */, apList.get(0), true /* isWiFiPermissionGranted */); 139 } 140 141 if (isApRowCollapsed()) { 142 return listBuilder.build(); 143 } 144 145 // Add AP rows 146 final CharSequence placeholder = mContext.getText(R.string.summary_placeholder); 147 for (int i = 0; i < DEFAULT_EXPANDED_ROW_COUNT; i++) { 148 if (i < apCount) { 149 listBuilder.addRow(getWifiSliceItemRow(apList.get(i))); 150 } else if (i == apCount) { 151 listBuilder.addRow(getLoadingRow(placeholder)); 152 } else { 153 listBuilder.addRow(new ListBuilder.RowBuilder() 154 .setTitle(placeholder) 155 .setSubtitle(placeholder)); 156 } 157 } 158 return listBuilder.build(); 159 } 160 isGuestUser(Context context)161 protected static boolean isGuestUser(Context context) { 162 if (context == null) return false; 163 final UserManager userManager = context.getSystemService(UserManager.class); 164 if (userManager == null) return false; 165 return userManager.isGuestUser(); 166 } 167 isCallerExemptUid(Context context)168 private boolean isCallerExemptUid(Context context) { 169 final String[] allowedUidNames = context.getResources().getStringArray( 170 R.array.config_exempt_wifi_permission_uid_name); 171 final String uidName = 172 context.getPackageManager().getNameForUid(Binder.getCallingUid()); 173 Log.d(TAG, "calling uid name : " + uidName); 174 175 for (String allowedUidName : allowedUidNames) { 176 if (TextUtils.equals(uidName, allowedUidName)) { 177 return true; 178 } 179 } 180 return false; 181 } 182 isPermissionGranted(Context settingsContext)183 private static boolean isPermissionGranted(Context settingsContext) { 184 final int callingUid = Binder.getCallingUid(); 185 final String callingPackage = settingsContext.getPackageManager() 186 .getPackagesForUid(callingUid)[0]; 187 188 Context packageContext; 189 try { 190 packageContext = settingsContext.createPackageContext(callingPackage, 0); 191 } catch (PackageManager.NameNotFoundException e) { 192 Log.e(TAG, "Cannot create Context for package: " + callingPackage); 193 return false; 194 } 195 196 // If app doesn't have related Wi-Fi permission, they shouldn't show Wi-Fi slice. 197 final boolean hasPermission = packageContext.checkPermission( 198 android.Manifest.permission.CHANGE_WIFI_STATE, Binder.getCallingPid(), 199 callingUid) == PackageManager.PERMISSION_GRANTED; 200 AppStateChangeWifiStateBridge.WifiSettingsState state = 201 new AppStateChangeWifiStateBridge(settingsContext, null, null) 202 .getWifiSettingsInfo(callingPackage, callingUid); 203 204 return hasPermission && state.isPermissible(); 205 } 206 isApRowCollapsed()207 protected boolean isApRowCollapsed() { 208 return false; 209 } 210 getHeaderRow(boolean isWifiEnabled, WifiSliceItem wifiSliceItem)211 protected ListBuilder.RowBuilder getHeaderRow(boolean isWifiEnabled, 212 WifiSliceItem wifiSliceItem) { 213 final IconCompat icon = IconCompat.createWithResource(mContext, 214 R.drawable.ic_settings_wireless); 215 final String title = mContext.getString(R.string.wifi_settings); 216 final PendingIntent primaryAction = getPrimaryAction(); 217 final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon, 218 ListBuilder.ICON_IMAGE, title); 219 220 final ListBuilder.RowBuilder builder = new ListBuilder.RowBuilder() 221 .setTitle(title) 222 .setPrimaryAction(primarySliceAction); 223 224 if (!mWifiRestriction.isChangeWifiStateAllowed(mContext)) { 225 builder.setSubtitle(mContext.getString(R.string.not_allowed_by_ent)); 226 } 227 return builder; 228 } 229 getListBuilder(boolean isWifiEnabled, WifiSliceItem wifiSliceItem, boolean isWiFiPermissionGranted)230 private ListBuilder getListBuilder(boolean isWifiEnabled, WifiSliceItem wifiSliceItem, 231 boolean isWiFiPermissionGranted) { 232 final ListBuilder builder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) 233 .setAccentColor(COLOR_NOT_TINTED) 234 .setKeywords(getKeywords()) 235 .addRow(getHeaderRow(isWifiEnabled, wifiSliceItem)); 236 if (!isWiFiPermissionGranted || !mWifiRestriction.isChangeWifiStateAllowed(mContext) 237 || isSatelliteOn()) { 238 return builder; 239 } 240 241 final PendingIntent toggleAction = getBroadcastIntent(mContext); 242 final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction, 243 null /* actionTitle */, isWifiEnabled); 244 builder.addAction(toggleSliceAction); 245 246 return builder; 247 } 248 getWifiSliceItemRow(WifiSliceItem wifiSliceItem)249 protected ListBuilder.RowBuilder getWifiSliceItemRow(WifiSliceItem wifiSliceItem) { 250 final CharSequence title = wifiSliceItem.getTitle(); 251 final IconCompat levelIcon = getWifiSliceItemLevelIcon(wifiSliceItem); 252 final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder() 253 .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE) 254 .setTitle(title) 255 .setSubtitle(wifiSliceItem.getSummary()) 256 .setContentDescription(wifiSliceItem.getContentDescription()) 257 .setPrimaryAction(getWifiEntryAction(wifiSliceItem, levelIcon, title)); 258 259 final IconCompat endIcon = getEndIcon(wifiSliceItem); 260 if (endIcon != null) { 261 rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE); 262 } 263 return rowBuilder; 264 } 265 getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem)266 protected IconCompat getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem) { 267 final @ColorInt int tint; 268 if (wifiSliceItem.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) { 269 tint = Utils.getColorAccentDefaultColor(mContext); 270 } else if (wifiSliceItem.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) { 271 tint = Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal); 272 } else { 273 tint = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext, 274 android.R.attr.colorControlNormal)); 275 } 276 277 Drawable drawable = mContext.getDrawable(getWifiIconResId(wifiSliceItem)); 278 drawable.setTint(tint); 279 return Utils.createIconWithDrawable(drawable); 280 } 281 282 @VisibleForTesting getWifiIconResId(WifiSliceItem wifiSliceItem)283 int getWifiIconResId(WifiSliceItem wifiSliceItem) { 284 return (wifiSliceItem.isInstantHotspotNetwork()) 285 ? getHotspotIconResource(wifiSliceItem.getInstantHotspotDeviceType()) 286 : WifiUtils.getInternetIconResource(wifiSliceItem.getLevel(), 287 wifiSliceItem.shouldShowXLevelIcon()); 288 } 289 getEndIcon(WifiSliceItem wifiSliceItem)290 protected IconCompat getEndIcon(WifiSliceItem wifiSliceItem) { 291 if (wifiSliceItem.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED) { 292 return IconCompat.createWithResource(mContext, R.drawable.ic_settings_24dp); 293 } 294 295 if (wifiSliceItem.getSecurity() != WifiEntry.SECURITY_NONE) { 296 return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed); 297 } 298 return null; 299 } 300 getWifiEntryAction(WifiSliceItem wifiSliceItem, IconCompat icon, CharSequence title)301 protected SliceAction getWifiEntryAction(WifiSliceItem wifiSliceItem, IconCompat icon, 302 CharSequence title) { 303 final int requestCode = wifiSliceItem.getKey().hashCode(); 304 305 if (wifiSliceItem.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED) { 306 final Bundle bundle = new Bundle(); 307 bundle.putString(WifiNetworkDetailsFragment.KEY_CHOSEN_WIFIENTRY_KEY, 308 wifiSliceItem.getKey()); 309 final Intent intent = new SubSettingLauncher(mContext) 310 .setTitleRes(R.string.pref_title_network_details) 311 .setDestination(WifiNetworkDetailsFragment.class.getName()) 312 .setArguments(bundle) 313 .setSourceMetricsCategory(SettingsEnums.WIFI) 314 .toIntent(); 315 return getActivityAction(requestCode, intent, icon, title); 316 } 317 318 if (wifiSliceItem.shouldEditBeforeConnect()) { 319 final Intent intent = new Intent(mContext, WifiDialogActivity.class) 320 .putExtra(WifiDialogActivity.KEY_CHOSEN_WIFIENTRY_KEY, wifiSliceItem.getKey()); 321 return getActivityAction(requestCode, intent, icon, title); 322 } 323 324 final Intent intent = new Intent(mContext, ConnectToWifiHandler.class) 325 .putExtra(ConnectToWifiHandler.KEY_CHOSEN_WIFIENTRY_KEY, wifiSliceItem.getKey()) 326 .putExtra(ConnectToWifiHandler.KEY_WIFI_SLICE_URI, getUri()); 327 return getBroadcastAction(requestCode, intent, icon, title); 328 } 329 getActivityAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)330 private SliceAction getActivityAction(int requestCode, Intent intent, IconCompat icon, 331 CharSequence title) { 332 final PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent, 333 PendingIntent.FLAG_IMMUTABLE /* flags */); 334 return SliceAction.createDeeplink(pi, icon, ListBuilder.ICON_IMAGE, title); 335 } 336 getBroadcastAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)337 private SliceAction getBroadcastAction(int requestCode, Intent intent, IconCompat icon, 338 CharSequence title) { 339 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 340 final PendingIntent pi = PendingIntent.getBroadcast(mContext, requestCode, intent, 341 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); 342 return SliceAction.create(pi, icon, ListBuilder.ICON_IMAGE, title); 343 } 344 getLoadingRow(CharSequence placeholder)345 private ListBuilder.RowBuilder getLoadingRow(CharSequence placeholder) { 346 final CharSequence title = mContext.getText(R.string.wifi_empty_list_wifi_on); 347 348 // for aligning to the Wi-Fi AP's name 349 final IconCompat emptyIcon = Utils.createIconWithDrawable( 350 new ColorDrawable(Color.TRANSPARENT)); 351 352 return new ListBuilder.RowBuilder() 353 .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE) 354 .setTitle(placeholder) 355 .setSubtitle(title); 356 } 357 358 /** 359 * Update the current wifi status to the boolean value keyed by 360 * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}. 361 */ 362 @Override onNotifyChange(Intent intent)363 public void onNotifyChange(Intent intent) { 364 final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, 365 mWifiManager.isWifiEnabled()); 366 mWifiManager.setWifiEnabled(newState); 367 // Do not notifyChange on Uri. The service takes longer to update the current value than it 368 // does for the Slice to check the current value again. Let {@link WifiScanWorker} 369 // handle it. 370 } 371 372 @Override getIntent()373 public Intent getIntent() { 374 final String screenTitle = mContext.getText(R.string.wifi_settings).toString(); 375 final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build(); 376 final String className = NetworkProviderSettings.class.getName(); 377 final String key = WifiSwitchPreferenceController.KEY; 378 379 final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, className, 380 key, screenTitle, SettingsEnums.DIALOG_WIFI_AP_EDIT, this) 381 .setClassName(mContext.getPackageName(), SubSettings.class.getName()) 382 .setData(contentUri); 383 384 return intent; 385 } 386 387 @Override getSliceHighlightMenuRes()388 public int getSliceHighlightMenuRes() { 389 return R.string.menu_key_network; 390 } 391 isWifiEnabled()392 private boolean isWifiEnabled() { 393 switch (mWifiManager.getWifiState()) { 394 case WifiManager.WIFI_STATE_ENABLED: 395 case WifiManager.WIFI_STATE_ENABLING: 396 return true; 397 default: 398 return false; 399 } 400 } 401 getPrimaryAction()402 private PendingIntent getPrimaryAction() { 403 final Intent intent = getIntent(); 404 return PendingIntent.getActivity(mContext, 0 /* requestCode */, 405 intent, PendingIntent.FLAG_IMMUTABLE /* flags */); 406 } 407 getKeywords()408 private Set<String> getKeywords() { 409 final String keywords = mContext.getString(R.string.keywords_wifi); 410 return Arrays.asList(TextUtils.split(keywords, ",")) 411 .stream() 412 .map(String::trim) 413 .collect(Collectors.toSet()); 414 } 415 416 @Override getBackgroundWorkerClass()417 public Class getBackgroundWorkerClass() { 418 return WifiScanWorker.class; 419 } 420 421 @VisibleForTesting 422 static class WifiRestriction { isChangeWifiStateAllowed(@ullable Context context)423 public boolean isChangeWifiStateAllowed(@Nullable Context context) { 424 if (context == null) return true; 425 return WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context); 426 } 427 } 428 isSatelliteOn()429 private boolean isSatelliteOn() { 430 SatelliteRepository satelliteRepository = new SatelliteRepository(mContext); 431 boolean isSatelliteOn = false; 432 try { 433 isSatelliteOn = 434 satelliteRepository.requestIsEnabled(Executors.newSingleThreadExecutor()) 435 .get(2000, TimeUnit.MILLISECONDS); 436 } catch (ExecutionException | TimeoutException | InterruptedException e) { 437 Log.e(TAG, "Error to get satellite status : " + e); 438 } 439 return isSatelliteOn; 440 } 441 } 442