• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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