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