1 /*
2  * Copyright (C) 2020 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.network;
18 
19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
20 
21 import static com.android.settings.slices.CustomSliceRegistry.PROVIDER_MODEL_SLICE_URI;
22 import static com.android.settings.slices.CustomSliceRegistry.WIFI_SLICE_URI;
23 
24 import android.annotation.ColorInt;
25 import android.app.AlertDialog;
26 import android.app.AlertDialog.Builder;
27 import android.app.PendingIntent;
28 import android.app.settings.SettingsEnums;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.SharedPreferences;
32 import android.graphics.Color;
33 import android.graphics.drawable.ColorDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.net.Uri;
36 import android.telephony.SubscriptionManager;
37 import android.util.Log;
38 import android.view.WindowManager.LayoutParams;
39 
40 import androidx.annotation.VisibleForTesting;
41 import androidx.core.graphics.drawable.IconCompat;
42 import androidx.slice.Slice;
43 import androidx.slice.builders.ListBuilder;
44 import androidx.slice.builders.SliceAction;
45 
46 import com.android.settings.R;
47 import com.android.settings.SubSettings;
48 import com.android.settings.Utils;
49 import com.android.settings.network.telephony.MobileNetworkUtils;
50 import com.android.settings.network.telephony.NetworkProviderWorker;
51 import com.android.settings.slices.CustomSliceable;
52 import com.android.settings.slices.SliceBackgroundWorker;
53 import com.android.settings.slices.SliceBroadcastReceiver;
54 import com.android.settings.slices.SliceBuilderUtils;
55 import com.android.settings.wifi.WifiUtils;
56 import com.android.settings.wifi.slice.WifiSlice;
57 import com.android.settings.wifi.slice.WifiSliceItem;
58 import com.android.wifitrackerlib.WifiEntry;
59 
60 import java.util.List;
61 import java.util.stream.Collectors;
62 
63 /**
64  * {@link CustomSliceable} for Wi-Fi and mobile data connection, used by generic clients.
65  */
66 // ToDo If the provider model become default design in the future, the code needs to refactor
67 // the whole structure and use new "data object", and then split provider model out of old design.
68 public class ProviderModelSlice extends WifiSlice {
69 
70     private static final String TAG = "ProviderModelSlice";
71     protected static final String PREF_NAME = "ProviderModelSlice";
72     protected static final String PREF_HAS_TURNED_OFF_MOBILE_DATA = "PrefHasTurnedOffMobileData";
73 
74     private final ProviderModelSliceHelper mHelper;
75     private final SharedPreferences mSharedPref;
76 
ProviderModelSlice(Context context)77     public ProviderModelSlice(Context context) {
78         super(context);
79         mHelper = getHelper();
80         mSharedPref = getSharedPreference();
81     }
82 
83     @Override
getUri()84     public Uri getUri() {
85         return PROVIDER_MODEL_SLICE_URI;
86     }
87 
log(String s)88     private static void log(String s) {
89         Log.d(TAG, s);
90     }
91 
isApRowCollapsed()92     protected boolean isApRowCollapsed() {
93         return false;
94     }
95 
96     @Override
getSlice()97     public Slice getSlice() {
98         // The provider model slice step:
99         //  First section: Add the Ethernet item.
100         // Second section: Add the carrier item.
101         //  Third section: Add the Wi-Fi toggle item.
102         // Fourth section: Add the connected Wi-Fi item.
103         //  Fifth section: Add the Wi-Fi items which are not connected.
104         //  Sixth section: Add the See All item.
105         final ListBuilder listBuilder = mHelper.createListBuilder(getUri());
106         int maxListSize = 0;
107         final NetworkProviderWorker worker = getWorker();
108         if (worker != null) {
109             maxListSize = worker.getApRowCount();
110         } else {
111             log("network provider worker is null.");
112         }
113 
114         // First section: Add the Ethernet item.
115         if (getInternetType() == InternetUpdater.INTERNET_ETHERNET) {
116             log("get Ethernet item which is connected");
117             listBuilder.addRow(createEthernetRow());
118             maxListSize--;
119         }
120 
121         // Second section: Add the carrier item.
122         if (!mHelper.isAirplaneModeEnabled()) {
123             final boolean hasCarrier = mHelper.hasCarrier();
124             log("hasCarrier: " + hasCarrier);
125             if (hasCarrier) {
126                 mHelper.updateTelephony();
127                 listBuilder.addRow(
128                         mHelper.createCarrierRow(
129                                 worker != null ? worker.getNetworkTypeDescription() : ""));
130                 maxListSize--;
131             }
132         }
133 
134         // Third section: Add the Wi-Fi toggle item.
135         final boolean isWifiEnabled = mWifiManager.isWifiEnabled();
136         listBuilder.addRow(createWifiToggleRow(mContext, isWifiEnabled));
137         maxListSize--;
138         if (!isWifiEnabled) {
139             log("Wi-Fi is disabled");
140             return listBuilder.build();
141         }
142         List<WifiSliceItem> wifiList = (worker != null) ? worker.getResults() : null;
143         if (wifiList == null || wifiList.size() <= 0) {
144             log("Wi-Fi list is empty");
145             return listBuilder.build();
146         }
147 
148         // Fourth section: Add the connected Wi-Fi item.
149         final WifiSliceItem connectedWifiItem = mHelper.getConnectedWifiItem(wifiList);
150         if (connectedWifiItem != null) {
151             log("get Wi-Fi item which is connected");
152             listBuilder.addRow(getWifiSliceItemRow(connectedWifiItem));
153             maxListSize--;
154         }
155 
156         // Fifth section: Add the Wi-Fi items which are not connected.
157         log("get Wi-Fi items which are not connected. Wi-Fi items : " + wifiList.size());
158         final List<WifiSliceItem> disconnectedWifiList = wifiList.stream()
159                 .filter(item -> item.getConnectedState() != WifiEntry.CONNECTED_STATE_CONNECTED)
160                 .limit(maxListSize - 1)
161                 .collect(Collectors.toList());
162         for (WifiSliceItem item : disconnectedWifiList) {
163             listBuilder.addRow(getWifiSliceItemRow(item));
164         }
165 
166         // Sixth section: Add the See All item.
167         log("add See-All");
168         listBuilder.addRow(getSeeAllRow());
169 
170         return listBuilder.build();
171     }
172 
173     @Override
getBroadcastIntent(Context context)174     public PendingIntent getBroadcastIntent(Context context) {
175         final Intent intent = new Intent(getUri().toString())
176                 // The FLAG_RECEIVER_FOREGROUND flag is necessary to avoid the intent delay of
177                 // the first sending after the device restarts
178                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
179                 .setData(getUri())
180                 .setClass(context, SliceBroadcastReceiver.class);
181         return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
182                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
183     }
184 
185     /**
186      * Update the current carrier's mobile data status.
187      */
188     @Override
onNotifyChange(Intent intent)189     public void onNotifyChange(Intent intent) {
190         final SubscriptionManager subscriptionManager = mHelper.getSubscriptionManager();
191         if (subscriptionManager == null) {
192             return;
193         }
194         final int defaultSubId = subscriptionManager.getDefaultDataSubscriptionId();
195         log("defaultSubId:" + defaultSubId);
196 
197         if (!defaultSubscriptionIsUsable(defaultSubId)) {
198             return;
199         }
200 
201         boolean isToggleAction = intent.hasExtra(EXTRA_TOGGLE_STATE);
202         boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE,
203                 mHelper.isMobileDataEnabled());
204 
205         if (isToggleAction) {
206             // The ToggleAction is used to set mobile data enabled.
207             if (!newState && mSharedPref != null
208                     && mSharedPref.getBoolean(PREF_HAS_TURNED_OFF_MOBILE_DATA, true)) {
209                 String carrierName = mHelper.getMobileTitle();
210                 if (carrierName.equals(mContext.getString(R.string.mobile_data_settings_title))) {
211                     carrierName = mContext.getString(
212                             R.string.mobile_data_disable_message_default_carrier);
213                 }
214                 showMobileDataDisableDialog(getMobileDataDisableDialog(defaultSubId, carrierName));
215                 // If we need to display a reminder dialog box, do nothing here.
216                 return;
217             } else {
218                 MobileNetworkUtils.setMobileDataEnabled(mContext, defaultSubId, newState,
219                         false /* disableOtherSubscriptions */);
220             }
221         }
222 
223         final boolean isDataEnabled =
224                 isToggleAction ? newState : MobileNetworkUtils.isMobileDataEnabled(mContext);
225         doCarrierNetworkAction(isToggleAction, isDataEnabled, defaultSubId);
226     }
227 
228     @VisibleForTesting
getMobileDataDisableDialog(int defaultSubId, String carrierName)229     AlertDialog getMobileDataDisableDialog(int defaultSubId, String carrierName) {
230         return new Builder(mContext)
231                 .setTitle(R.string.mobile_data_disable_title)
232                 .setMessage(mContext.getString(R.string.mobile_data_disable_message,
233                         carrierName))
234                 .setNegativeButton(android.R.string.cancel,
235                         (dialog, which) -> {
236                             // Because the toggle of mobile data will be turned off first, if the
237                             // user cancels the operation, we need to update the slice to correct
238                             // the toggle state.
239                             final NetworkProviderWorker worker = getWorker();
240                             if (worker != null) {
241                                 worker.updateSlice();
242                             }
243                         })
244                 .setPositiveButton(
245                         com.android.internal.R.string.alert_windows_notification_turn_off_action,
246                         (dialog, which) -> {
247                             MobileNetworkUtils.setMobileDataEnabled(mContext, defaultSubId,
248                                     false /* enabled */,
249                                     false /* disableOtherSubscriptions */);
250                             doCarrierNetworkAction(true /* isToggleAction */,
251                                     false /* isDataEanbed */, defaultSubId);
252                             if (mSharedPref != null) {
253                                 SharedPreferences.Editor editor = mSharedPref.edit();
254                                 editor.putBoolean(PREF_HAS_TURNED_OFF_MOBILE_DATA, false);
255                                 editor.apply();
256                             }
257                         })
258                 .create();
259     }
260 
showMobileDataDisableDialog(AlertDialog dialog)261     private void showMobileDataDisableDialog(AlertDialog dialog) {
262         if (dialog == null) {
263             log("AlertDialog is null");
264             return;
265         }
266 
267         dialog.getWindow().setType(LayoutParams.TYPE_KEYGUARD_DIALOG);
268         dialog.show();
269     }
270 
271     @VisibleForTesting
doCarrierNetworkAction(boolean isToggleAction, boolean isDataEnabled, int subId)272     void doCarrierNetworkAction(boolean isToggleAction, boolean isDataEnabled, int subId) {
273         final NetworkProviderWorker worker = getWorker();
274         if (worker == null) {
275             return;
276         }
277 
278         if (isToggleAction) {
279             worker.setCarrierNetworkEnabledIfNeeded(isDataEnabled, subId);
280             return;
281         }
282 
283         if (isDataEnabled) {
284             worker.connectCarrierNetwork();
285         }
286     }
287 
288     @Override
getIntent()289     public Intent getIntent() {
290         final String screenTitle = mContext.getText(R.string.provider_internet_settings).toString();
291         return SliceBuilderUtils.buildSearchResultPageIntent(mContext,
292                 NetworkProviderSettings.class.getName(), "" /* key */, screenTitle,
293                 SettingsEnums.SLICE, this)
294                 .setClassName(mContext.getPackageName(), SubSettings.class.getName())
295                 .setData(getUri());
296     }
297 
298     @Override
getBackgroundWorkerClass()299     public Class getBackgroundWorkerClass() {
300         return NetworkProviderWorker.class;
301     }
302 
303     @VisibleForTesting
getHelper()304     ProviderModelSliceHelper getHelper() {
305         return new ProviderModelSliceHelper(mContext, this);
306     }
307 
308     @VisibleForTesting
getWorker()309     NetworkProviderWorker getWorker() {
310         return SliceBackgroundWorker.getInstance(getUri());
311     }
312 
313     @VisibleForTesting
getSharedPreference()314     SharedPreferences getSharedPreference() {
315         return mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
316     }
317 
getInternetType()318     private @InternetUpdater.InternetType int getInternetType() {
319         final NetworkProviderWorker worker = getWorker();
320         if (worker == null) {
321             return InternetUpdater.INTERNET_NETWORKS_AVAILABLE;
322         }
323         return worker.getInternetType();
324     }
325 
326     @VisibleForTesting
createEthernetRow()327     ListBuilder.RowBuilder createEthernetRow() {
328         final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder();
329         final Drawable drawable = mContext.getDrawable(R.drawable.ic_settings_ethernet);
330         if (drawable != null) {
331             drawable.setTintList(Utils.getColorAttr(mContext, android.R.attr.colorAccent));
332             rowBuilder.setTitleItem(Utils.createIconWithDrawable(drawable), ListBuilder.ICON_IMAGE);
333         }
334         return rowBuilder
335                 .setTitle(mContext.getText(R.string.ethernet))
336                 .setSubtitle(mContext.getText(R.string.to_switch_networks_disconnect_ethernet));
337     }
338 
339     /**
340      * @return a {@link ListBuilder.RowBuilder} of the Wi-Fi toggle.
341      */
createWifiToggleRow(Context context, boolean isWifiEnabled)342     protected ListBuilder.RowBuilder createWifiToggleRow(Context context, boolean isWifiEnabled) {
343         final Intent intent = new Intent(WIFI_SLICE_URI.toString())
344                 .setData(WIFI_SLICE_URI)
345                 .setClass(context, SliceBroadcastReceiver.class)
346                 .putExtra(EXTRA_TOGGLE_STATE, !isWifiEnabled)
347                 // The FLAG_RECEIVER_FOREGROUND flag is necessary to avoid the intent delay of
348                 // the first sending after the device restarts
349                 .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
350         final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent,
351                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
352         final SliceAction toggleSliceAction = SliceAction.createToggle(pendingIntent,
353                 null /* actionTitle */, isWifiEnabled);
354         return new ListBuilder.RowBuilder()
355                 .setTitle(context.getString(R.string.wifi_settings))
356                 .setPrimaryAction(toggleSliceAction);
357     }
358 
getSeeAllRow()359     protected ListBuilder.RowBuilder getSeeAllRow() {
360         final CharSequence title = mContext.getText(R.string.previous_connected_see_all);
361         final IconCompat icon = getSeeAllIcon();
362         return new ListBuilder.RowBuilder()
363                 .setTitleItem(icon, ListBuilder.ICON_IMAGE)
364                 .setTitle(title)
365                 .setPrimaryAction(getPrimaryAction(icon, title));
366     }
367 
getSeeAllIcon()368     protected IconCompat getSeeAllIcon() {
369         final Drawable drawable = mContext.getDrawable(R.drawable.ic_arrow_forward);
370         if (drawable != null) {
371             drawable.setTint(
372                     Utils.getColorAttrDefaultColor(mContext, android.R.attr.colorControlNormal));
373             return Utils.createIconWithDrawable(drawable);
374         }
375         return Utils.createIconWithDrawable(new ColorDrawable(Color.TRANSPARENT));
376     }
377 
getPrimaryAction(IconCompat icon, CharSequence title)378     protected SliceAction getPrimaryAction(IconCompat icon, CharSequence title) {
379         final PendingIntent intent = PendingIntent.getActivity(mContext, 0 /* requestCode */,
380                 getIntent(), PendingIntent.FLAG_IMMUTABLE /* flags */);
381         return SliceAction.createDeeplink(intent, icon, ListBuilder.ICON_IMAGE, title);
382     }
383 
384     @Override
getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem)385     protected IconCompat getWifiSliceItemLevelIcon(WifiSliceItem wifiSliceItem) {
386         if (wifiSliceItem.getConnectedState() == WifiEntry.CONNECTED_STATE_CONNECTED
387                 && getInternetType() != InternetUpdater.INTERNET_WIFI) {
388             final @ColorInt int tint = Utils.getColorAttrDefaultColor(mContext,
389                     android.R.attr.colorControlNormal);
390             final Drawable drawable = mContext.getDrawable(
391                     WifiUtils.getInternetIconResource(
392                             wifiSliceItem.getLevel(), wifiSliceItem.shouldShowXLevelIcon()));
393             drawable.setTint(tint);
394             return Utils.createIconWithDrawable(drawable);
395         }
396         return super.getWifiSliceItemLevelIcon(wifiSliceItem);
397     }
398 
399     /**
400      * Wrap the subscriptionManager call for test mocking.
401      */
402     @VisibleForTesting
defaultSubscriptionIsUsable(int defaultSubId)403     protected boolean defaultSubscriptionIsUsable(int defaultSubId) {
404         return SubscriptionManager.isUsableSubscriptionId(defaultSubId);
405     }
406 }
407