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.drawable.ColorDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.net.Uri;
33 import android.net.wifi.WifiManager;
34 import android.os.Bundle;
35 import android.text.TextUtils;
36 
37 import androidx.annotation.VisibleForTesting;
38 import androidx.core.graphics.drawable.IconCompat;
39 import androidx.slice.Slice;
40 import androidx.slice.builders.ListBuilder;
41 import androidx.slice.builders.SliceAction;
42 
43 import com.android.settings.R;
44 import com.android.settings.SubSettings;
45 import com.android.settings.Utils;
46 import com.android.settings.core.SubSettingLauncher;
47 import com.android.settings.network.NetworkProviderSettings;
48 import com.android.settings.network.WifiSwitchPreferenceController;
49 import com.android.settings.slices.CustomSliceable;
50 import com.android.settings.slices.SliceBackgroundWorker;
51 import com.android.settings.slices.SliceBuilderUtils;
52 import com.android.settings.wifi.WifiDialogActivity;
53 import com.android.settings.wifi.WifiSettings;
54 import com.android.settings.wifi.WifiUtils;
55 import com.android.settings.wifi.details.WifiNetworkDetailsFragment;
56 import com.android.wifitrackerlib.WifiEntry;
57 
58 import java.util.Arrays;
59 import java.util.List;
60 import java.util.Set;
61 import java.util.stream.Collectors;
62 
63 /**
64  * {@link CustomSliceable} for Wi-Fi, used by generic clients.
65  */
66 public class WifiSlice implements CustomSliceable {
67 
68     @VisibleForTesting
69     static final int DEFAULT_EXPANDED_ROW_COUNT = 3;
70 
71     protected final Context mContext;
72     protected final WifiManager mWifiManager;
73 
WifiSlice(Context context)74     public WifiSlice(Context context) {
75         mContext = context;
76         mWifiManager = mContext.getSystemService(WifiManager.class);
77     }
78 
79     @Override
getUri()80     public Uri getUri() {
81         return WIFI_SLICE_URI;
82     }
83 
84     @Override
getSlice()85     public Slice getSlice() {
86         final boolean isWifiEnabled = isWifiEnabled();
87         ListBuilder listBuilder = getListBuilder(isWifiEnabled, null /* wifiSliceItem */);
88         if (!isWifiEnabled) {
89             return listBuilder.build();
90         }
91 
92         final WifiScanWorker worker = SliceBackgroundWorker.getInstance(getUri());
93         final List<WifiSliceItem> apList = worker != null ? worker.getResults() : null;
94         final int apCount = apList == null ? 0 : apList.size();
95         final boolean isFirstApActive = apCount > 0
96                 && apList.get(0).getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED;
97 
98         if (isFirstApActive) {
99             // refresh header subtext
100             listBuilder = getListBuilder(true /* isWifiEnabled */, apList.get(0));
101         }
102 
103         if (isApRowCollapsed()) {
104             return listBuilder.build();
105         }
106 
107         // Add AP rows
108         final CharSequence placeholder = mContext.getText(R.string.summary_placeholder);
109         for (int i = 0; i < DEFAULT_EXPANDED_ROW_COUNT; i++) {
110             if (i < apCount) {
111                 listBuilder.addRow(getWifiSliceItemRow(apList.get(i)));
112             } else if (i == apCount) {
113                 listBuilder.addRow(getLoadingRow(placeholder));
114             } else {
115                 listBuilder.addRow(new ListBuilder.RowBuilder()
116                         .setTitle(placeholder)
117                         .setSubtitle(placeholder));
118             }
119         }
120         return listBuilder.build();
121     }
122 
isApRowCollapsed()123     protected boolean isApRowCollapsed() {
124         return false;
125     }
126 
getHeaderRow(boolean isWifiEnabled, WifiSliceItem wifiSliceItem)127     protected ListBuilder.RowBuilder getHeaderRow(boolean isWifiEnabled,
128             WifiSliceItem wifiSliceItem) {
129         final IconCompat icon = IconCompat.createWithResource(mContext,
130                 R.drawable.ic_settings_wireless);
131         final String title = mContext.getString(R.string.wifi_settings);
132         final PendingIntent primaryAction = getPrimaryAction();
133         final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon,
134                 ListBuilder.ICON_IMAGE, title);
135 
136         return new ListBuilder.RowBuilder()
137                 .setTitle(title)
138                 .setPrimaryAction(primarySliceAction);
139     }
140 
getListBuilder(boolean isWifiEnabled, WifiSliceItem wifiSliceItem)141     private ListBuilder getListBuilder(boolean isWifiEnabled, WifiSliceItem wifiSliceItem) {
142         final PendingIntent toggleAction = getBroadcastIntent(mContext);
143         final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction,
144                 null /* actionTitle */, isWifiEnabled);
145         final ListBuilder builder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY)
146                 .setAccentColor(COLOR_NOT_TINTED)
147                 .setKeywords(getKeywords())
148                 .addRow(getHeaderRow(isWifiEnabled, wifiSliceItem))
149                 .addAction(toggleSliceAction);
150         return builder;
151     }
152 
getWifiSliceItemRow(WifiSliceItem wifiSliceItem)153     protected ListBuilder.RowBuilder getWifiSliceItemRow(WifiSliceItem wifiSliceItem) {
154         final CharSequence title = wifiSliceItem.getTitle();
155         final IconCompat levelIcon = getWifiSliceItemLevelIcon(wifiSliceItem);
156         final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
157                 .setTitleItem(levelIcon, ListBuilder.ICON_IMAGE)
158                 .setTitle(title)
159                 .setSubtitle(wifiSliceItem.getSummary())
160                 .setContentDescription(wifiSliceItem.getContentDescription())
161                 .setPrimaryAction(getWifiEntryAction(wifiSliceItem, levelIcon, title));
162 
163         final IconCompat endIcon = getEndIcon(wifiSliceItem);
164         if (endIcon != null) {
165             rowBuilder.addEndItem(endIcon, ListBuilder.ICON_IMAGE);
166         }
167         return rowBuilder;
168     }
169 
getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem)170     protected IconCompat getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem) {
171         final @ColorInt int tint;
172         if (wifiSliceItem.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED) {
173             tint = Utils.getColorAccentDefaultColor(mContext);
174         } else if (wifiSliceItem.getConnectedState() == WifiEntry.CONNECTED_STATE_DISCONNECTED) {
175             tint = Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal);
176         } else {
177             tint = Utils.getDisabled(mContext, Utils.getColorAttrDefaultColor(mContext,
178                         android.R.attr.colorControlNormal));
179         }
180 
181         final Drawable drawable = mContext.getDrawable(
182                 WifiUtils.getInternetIconResource(wifiSliceItem.getLevel(),
183                         wifiSliceItem.shouldShowXLevelIcon()));
184         drawable.setTint(tint);
185         return Utils.createIconWithDrawable(drawable);
186     }
187 
getEndIcon(WifiSliceItem wifiSliceItem)188     protected IconCompat getEndIcon(WifiSliceItem wifiSliceItem) {
189         if (wifiSliceItem.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED) {
190             return IconCompat.createWithResource(mContext, R.drawable.ic_settings_24dp);
191         }
192 
193         if (wifiSliceItem.getSecurity() != WifiEntry.SECURITY_NONE) {
194             return IconCompat.createWithResource(mContext, R.drawable.ic_friction_lock_closed);
195         }
196         return null;
197     }
198 
getWifiEntryAction(WifiSliceItem wifiSliceItem, IconCompat icon, CharSequence title)199     protected SliceAction getWifiEntryAction(WifiSliceItem wifiSliceItem, IconCompat icon,
200             CharSequence title) {
201         final int requestCode = wifiSliceItem.getKey().hashCode();
202 
203         if (wifiSliceItem.getConnectedState() != WifiEntry.CONNECTED_STATE_DISCONNECTED) {
204             final Bundle bundle = new Bundle();
205             bundle.putString(WifiNetworkDetailsFragment.KEY_CHOSEN_WIFIENTRY_KEY,
206                     wifiSliceItem.getKey());
207             final Intent intent = new SubSettingLauncher(mContext)
208                     .setTitleRes(R.string.pref_title_network_details)
209                     .setDestination(WifiNetworkDetailsFragment.class.getName())
210                     .setArguments(bundle)
211                     .setSourceMetricsCategory(SettingsEnums.WIFI)
212                     .toIntent();
213             return getActivityAction(requestCode, intent, icon, title);
214         }
215 
216         if (wifiSliceItem.shouldEditBeforeConnect()) {
217             final Intent intent = new Intent(mContext, WifiDialogActivity.class)
218                     .putExtra(WifiDialogActivity.KEY_CHOSEN_WIFIENTRY_KEY, wifiSliceItem.getKey());
219             return getActivityAction(requestCode, intent, icon, title);
220         }
221 
222         final Intent intent = new Intent(mContext, ConnectToWifiHandler.class)
223                 .putExtra(ConnectToWifiHandler.KEY_CHOSEN_WIFIENTRY_KEY, wifiSliceItem.getKey())
224                 .putExtra(ConnectToWifiHandler.KEY_WIFI_SLICE_URI, getUri());
225         return getBroadcastAction(requestCode, intent, icon, title);
226     }
227 
getActivityAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)228     private SliceAction getActivityAction(int requestCode, Intent intent, IconCompat icon,
229             CharSequence title) {
230         final PendingIntent pi = PendingIntent.getActivity(mContext, requestCode, intent,
231                 PendingIntent.FLAG_IMMUTABLE /* flags */);
232         return SliceAction.createDeeplink(pi, icon, ListBuilder.ICON_IMAGE, title);
233     }
234 
getBroadcastAction(int requestCode, Intent intent, IconCompat icon, CharSequence title)235     private SliceAction getBroadcastAction(int requestCode, Intent intent, IconCompat icon,
236             CharSequence title) {
237         intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
238         final PendingIntent pi = PendingIntent.getBroadcast(mContext, requestCode, intent,
239                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
240         return SliceAction.create(pi, icon, ListBuilder.ICON_IMAGE, title);
241     }
242 
getLoadingRow(CharSequence placeholder)243     private ListBuilder.RowBuilder getLoadingRow(CharSequence placeholder) {
244         final CharSequence title = mContext.getText(R.string.wifi_empty_list_wifi_on);
245 
246         // for aligning to the Wi-Fi AP's name
247         final IconCompat emptyIcon = Utils.createIconWithDrawable(
248                 new ColorDrawable(Color.TRANSPARENT));
249 
250         return new ListBuilder.RowBuilder()
251                 .setTitleItem(emptyIcon, ListBuilder.ICON_IMAGE)
252                 .setTitle(placeholder)
253                 .setSubtitle(title);
254     }
255 
256     /**
257      * Update the current wifi status to the boolean value keyed by
258      * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}.
259      */
260     @Override
onNotifyChange(Intent intent)261     public void onNotifyChange(Intent intent) {
262         final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE,
263                 mWifiManager.isWifiEnabled());
264         mWifiManager.setWifiEnabled(newState);
265         // Do not notifyChange on Uri. The service takes longer to update the current value than it
266         // does for the Slice to check the current value again. Let {@link WifiScanWorker}
267         // handle it.
268     }
269 
270     @Override
getIntent()271     public Intent getIntent() {
272         final String screenTitle = mContext.getText(R.string.wifi_settings).toString();
273         final Uri contentUri = new Uri.Builder().appendPath(KEY_WIFI).build();
274         final String className = NetworkProviderSettings.class.getName();
275         final String key = WifiSwitchPreferenceController.KEY;
276 
277         final Intent intent = SliceBuilderUtils.buildSearchResultPageIntent(mContext, className,
278                 key, screenTitle, SettingsEnums.DIALOG_WIFI_AP_EDIT, this)
279                 .setClassName(mContext.getPackageName(), SubSettings.class.getName())
280                 .setData(contentUri);
281 
282         return intent;
283     }
284 
285     @Override
getSliceHighlightMenuRes()286     public int getSliceHighlightMenuRes() {
287         return R.string.menu_key_network;
288     }
289 
isWifiEnabled()290     private boolean isWifiEnabled() {
291         switch (mWifiManager.getWifiState()) {
292             case WifiManager.WIFI_STATE_ENABLED:
293             case WifiManager.WIFI_STATE_ENABLING:
294                 return true;
295             default:
296                 return false;
297         }
298     }
299 
getPrimaryAction()300     private PendingIntent getPrimaryAction() {
301         final Intent intent = getIntent();
302         return PendingIntent.getActivity(mContext, 0 /* requestCode */,
303                 intent, PendingIntent.FLAG_IMMUTABLE /* flags */);
304     }
305 
getKeywords()306     private Set<String> getKeywords() {
307         final String keywords = mContext.getString(R.string.keywords_wifi);
308         return Arrays.asList(TextUtils.split(keywords, ","))
309                 .stream()
310                 .map(String::trim)
311                 .collect(Collectors.toSet());
312     }
313 
314     @Override
getBackgroundWorkerClass()315     public Class getBackgroundWorkerClass() {
316         return WifiScanWorker.class;
317     }
318 }
319