2 * Copyright (C) 2018 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.wifi.slice;
19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
20 import static android.provider.SettingsSlicesContract.KEY_WIFI;
22 import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
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;
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;
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;
61 import java.util.Arrays;
62 import java.util.List;
64 import java.util.stream.Collectors;
67 * {@link CustomSliceable} for Wi-Fi, used by generic clients.
69 public class WifiSlice implements CustomSliceable {
72 static final int DEFAULT_EXPANDED_ROW_COUNT = 3;
74 protected final Context mContext;
75 protected final WifiManager mWifiManager;
76 protected final ConnectivityManager mConnectivityManager;
78 public WifiSlice(Context context) {
80 mWifiManager = mContext.getSystemService(WifiManager.class);
81 mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
86 return WIFI_SLICE_URI;
90 public Slice getSlice() {
91 // Reload theme for switching dark mode on/off
92 mContext.getTheme().applyStyle(R.style.Theme_Settings_Home, true /* force */);
94 final boolean isWifiEnabled = isWifiEnabled();
95 ListBuilder listBuilder = getHeaderRow(isWifiEnabled);
97 return listBuilder.build();
100 final WifiScanWorker worker = SliceBackgroundWorker.getInstance(getUri());
101 final List<AccessPoint> apList = worker != null ? worker.getResults() : null;
102 final int apCount = apList == null ? 0 : apList.size();
103 final boolean isFirstApActive = apCount > 0 && apList.get(0).isActive();
104 handleNetworkCallback(worker, isFirstApActive);
106 // Need a loading text when results are not ready or out of date.
107 boolean needLoadingRow = true;
108 // Skip checking the existence of the first access point if it's active
109 int index = isFirstApActive ? 1 : 0;
110 // This loop checks the existence of reachable APs to determine the validity of the current
112 for (; index < apCount; index++) {
113 if (apList.get(index).isReachable()) {
114 needLoadingRow = false;
120 final CharSequence placeholder = mContext.getText(R.string.summary_placeholder);
121 for (int i = 0; i < DEFAULT_EXPANDED_ROW_COUNT; i++) {
123 listBuilder.addRow(getAccessPointRow(apList.get(i)));
124 } else if (needLoadingRow) {
125 listBuilder.addRow(getLoadingRow(placeholder));
126 needLoadingRow = false;
128 listBuilder.addRow(new ListBuilder.RowBuilder()
129 .setTitle(placeholder)
130 .setSubtitle(placeholder));
133 return listBuilder.build();
136 private ListBuilder getHeaderRow(boolean isWifiEnabled) {
137 final IconCompat icon = IconCompat.createWithResource(mContext,
138 R.drawable.ic_settings_wireless);
139 final String title = mContext.getString(R.string.wifi_settings);
140 final CharSequence summary = getSummary();
141 final PendingIntent toggleAction = getBroadcastIntent(mContext);
142 final PendingIntent primaryAction = getPrimaryAction();
143 final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon,
144 ListBuilder.ICON_IMAGE, title);
145 final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction,
146 null /* actionTitle */, isWifiEnabled);
148 return new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
149 .setAccentColor(COLOR_NOT_TINTED)
150 .setKeywords(getKeywords())
151 .addRow(new ListBuilder.RowBuilder()
153 .setSubtitle(summary)
154 .addEndItem(toggleSliceAction)
155 .setPrimaryAction(primarySliceAction));
158 private void handleNetworkCallback(WifiScanWorker worker, boolean isFirstApActive) {
159 if (worker == null) {
162 if (isFirstApActive) {
163 worker.registerNetworkCallback(mWifiManager.getCurrentNetwork());
165 worker.unregisterNetworkCallback();
169 private ListBuilder.RowBuilder getAccessPointRow(AccessPoint accessPoint) {
170 final boolean isCaptivePortal = accessPoint.isActive() && isCaptivePortal();
171 final CharSequence title = accessPoint.getTitle();
172 final CharSequence summary = getAccessPointSummary(accessPoint, isCaptivePortal);
173 final IconCompat levelIcon = getAccessPointLevelIcon(accessPoint);
174 final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
175 .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE)
177 .setSubtitle(summary)
178 .setPrimaryAction(SliceAction.createDeeplink(
179 getAccessPointAction(accessPoint, isCaptivePortal), levelIcon,
180 ListBuilder.ICON_IMAGE, title));
182 if (isCaptivePortal) {
183 rowBuilder.addEndItem(getCaptivePortalEndAction(accessPoint, title));
185 final IconCompat endIcon = getEndIcon(accessPoint);
186 if (endIcon != null) {
187 rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE);
193 private CharSequence getAccessPointSummary(AccessPoint accessPoint, boolean isCaptivePortal) {
194 if (isCaptivePortal) {
195 return mContext.getText(R.string.wifi_tap_to_sign_in);
198 final CharSequence summary = accessPoint.getSettingsSummary();
199 return TextUtils.isEmpty(summary) ? mContext.getText(R.string.disconnected) : summary;
202 private IconCompat getAccessPointLevelIcon(AccessPoint accessPoint) {
203 final Drawable d = mContext.getDrawable(
204 com.android.settingslib.Utils.getWifiIconResource(accessPoint.getLevel()));
207 if (accessPoint.isActive()) {
208 final NetworkInfo.State state = accessPoint.getNetworkInfo().getState();
209 if (state == NetworkInfo.State.CONNECTED) {
210 color = Utils.getColorAccentDefaultColor(mContext);
211 } else { // connecting
212 color = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext,
213 android.R.attr.colorControlNormal));
216 color = Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal);
219 d.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
220 return Utils.createIconWithDrawable(d);
223 private IconCompat getEndIcon(AccessPoint accessPoint) {
224 if (accessPoint.isActive()) {
226 } else if (accessPoint.getSecurity() != AccessPoint.SECURITY_NONE) {
227 return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed);
228 } else if (accessPoint.isMetered()) {
229 return IconCompat.createWithResource(mContext, R.drawable.ic_friction_money);
234 private SliceAction getCaptivePortalEndAction(AccessPoint accessPoint, CharSequence title) {
235 return SliceAction.createDeeplink(
236 getAccessPointAction(accessPoint, false /* isCaptivePortal */),
237 IconCompat.createWithResource(mContext, R.drawable.ic_settings_accent),
238 ListBuilder.ICON_IMAGE, title);
241 private PendingIntent getAccessPointAction(AccessPoint accessPoint, boolean isCaptivePortal) {
242 final Bundle extras = new Bundle();
243 accessPoint.saveWifiState(extras);
246 if (isCaptivePortal) {
247 intent = new Intent(mContext, ConnectToWifiHandler.class);
248 intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mWifiManager.getCurrentNetwork());
249 } else if (accessPoint.isActive()) {
250 intent = new SubSettingLauncher(mContext)
251 .setTitleRes(R.string.pref_title_network_details)
252 .setDestination(WifiNetworkDetailsFragment.class.getName())
253 .setArguments(extras)
254 .setSourceMetricsCategory(SettingsEnums.WIFI)
256 } else if (WifiUtils.getConnectingType(accessPoint) != WifiUtils.CONNECT_TYPE_OTHERS) {
257 intent = new Intent(mContext, ConnectToWifiHandler.class);
258 intent.putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras);
260 intent = new Intent(mContext, WifiDialogActivity.class);
261 intent.putExtra(WifiDialogActivity.KEY_ACCESS_POINT_STATE, extras);
263 return PendingIntent.getActivity(mContext, accessPoint.hashCode() /* requestCode */,
264 intent, 0 /* flags */);
267 private ListBuilder.RowBuilder getLoadingRow(CharSequence placeholder) {
268 final CharSequence title = mContext.getText(R.string.wifi_empty_list_wifi_on);
270 // for aligning to the Wi-Fi AP's name
271 final IconCompat emptyIcon = Utils.createIconWithDrawable(
272 new ColorDrawable(Color.TRANSPARENT));
274 return new ListBuilder.RowBuilder()
275 .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE)
276 .setTitle(placeholder)
280 protected boolean isCaptivePortal() {
281 final NetworkCapabilities nc = mConnectivityManager.getNetworkCapabilities(
282 mWifiManager.getCurrentNetwork());
283 return WifiUtils.canSignIntoNetwork(nc);
287 * Update the current wifi status to the boolean value keyed by
288 * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}.
291 public void onNotifyChange(Intent intent) {
292 final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE,
293 mWifiManager.isWifiEnabled());
294 mWifiManager.setWifiEnabled(newState);
295 // Do not notifyChange on Uri. The service takes longer to update the current value than it
296 // does for the Slice to check the current value again. Let {@link WifiScanWorker}
301 public Intent getIntent() {
302 final String screenTitle = mContext.getText(R.string.wifi_settings).toString();
303 final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build();
304 final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext,
305 WifiSettings.class.getName(), KEY_WIFI, screenTitle,
306 SettingsEnums.DIALOG_WIFI_AP_EDIT)
307 .setClassName(mContext.getPackageName(), SubSettings.class.getName())
308 .setData(contentUri);
313 private boolean isWifiEnabled() {
314 switch (mWifiManager.getWifiState()) {
315 case WifiManager.WIFI_STATE_ENABLED:
316 case WifiManager.WIFI_STATE_ENABLING:
323 private CharSequence getSummary() {
324 switch (mWifiManager.getWifiState()) {
325 case WifiManager.WIFI_STATE_ENABLED:
326 case WifiManager.WIFI_STATE_ENABLING:
327 return mContext.getText(R.string.switch_on_text);
328 case WifiManager.WIFI_STATE_DISABLED:
329 case WifiManager.WIFI_STATE_DISABLING:
330 return mContext.getText(R.string.switch_off_text);
331 case WifiManager.WIFI_STATE_UNKNOWN:
337 private PendingIntent getPrimaryAction() {
338 final Intent intent = getIntent();
339 return PendingIntent.getActivity(mContext, 0 /* requestCode */,
340 intent, 0 /* flags */);
343 private Set<String> getKeywords() {
344 final String keywords = mContext.getString(R.string.keywords_wifi);
345 return Arrays.asList(TextUtils.split(keywords, ","))
348 .collect(Collectors.toSet());
352 public Class getBackgroundWorkerClass() {
353 return WifiScanWorker.class;