1 /*
2  * Copyright (C) 2019 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.telephony;
18 
19 import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
20 
21 import android.annotation.ColorInt;
22 import android.app.PendingIntent;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.database.ContentObserver;
27 import android.net.Uri;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.telephony.SubscriptionInfo;
31 import android.telephony.SubscriptionManager;
32 import android.telephony.TelephonyManager;
33 
34 import androidx.core.graphics.drawable.IconCompat;
35 import androidx.slice.Slice;
36 import androidx.slice.builders.ListBuilder;
37 import androidx.slice.builders.SliceAction;
38 
39 import com.android.settings.R;
40 import com.android.settings.Utils;
41 import com.android.settings.network.MobileDataContentObserver;
42 import com.android.settings.network.SubscriptionUtil;
43 import com.android.settings.slices.CustomSliceRegistry;
44 import com.android.settings.slices.CustomSliceable;
45 import com.android.settings.slices.SliceBackgroundWorker;
46 import com.android.settingslib.WirelessUtils;
47 
48 import com.google.common.annotations.VisibleForTesting;
49 
50 import java.io.IOException;
51 import java.util.List;
52 
53 /**
54  * Custom {@link Slice} for Mobile Data.
55  * <p>
56  *     We make a custom slice instead of using {@link MobileDataPreferenceController} because the
57  *     pref controller is generalized across any carrier, and thus does not control a specific
58  *     subscription. We attempt to reuse any telephony-specific code from the preference controller.
59  *
60  * </p>
61  *
62  */
63 public class MobileDataSlice implements CustomSliceable {
64 
65     private final Context mContext;
66     private final SubscriptionManager mSubscriptionManager;
67     private final TelephonyManager mTelephonyManager;
68 
MobileDataSlice(Context context)69     public MobileDataSlice(Context context) {
70         mContext = context;
71         mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
72         mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
73     }
74 
75     @Override
getSlice()76     public Slice getSlice() {
77         final IconCompat icon = IconCompat.createWithResource(mContext,
78                 R.drawable.ic_network_cell);
79         final String title = mContext.getText(R.string.mobile_data_settings_title).toString();
80         @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
81 
82         // Return null until we can show a disabled-action Slice, blaming Airplane mode.
83         if (isAirplaneModeEnabled()) {
84             return null;
85         }
86 
87         // Return null until we can show a disabled-action Slice.
88         if (!isMobileDataAvailable()) {
89             return null;
90         }
91 
92         final CharSequence summary = getSummary();
93         final PendingIntent toggleAction = getBroadcastIntent(mContext);
94         final PendingIntent primaryAction = getPrimaryAction();
95         final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon,
96                 ListBuilder.ICON_IMAGE, title);
97         final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction,
98                 null /* actionTitle */, isMobileDataEnabled());
99         final ListBuilder.RowBuilder rowBuilder = new ListBuilder.RowBuilder()
100                 .setTitle(title)
101                 .addEndItem(toggleSliceAction)
102                 .setPrimaryAction(primarySliceAction);
103         if (!Utils.isSettingsIntelligence(mContext)) {
104             rowBuilder.setSubtitle(summary);
105         }
106 
107         final ListBuilder listBuilder = new ListBuilder(mContext, getUri(),
108                 ListBuilder.INFINITY)
109                 .setAccentColor(color)
110                 .addRow(rowBuilder);
111         return listBuilder.build();
112     }
113 
114     @Override
getUri()115     public Uri getUri() {
116         return CustomSliceRegistry.MOBILE_DATA_SLICE_URI;
117     }
118 
119     @Override
onNotifyChange(Intent intent)120     public void onNotifyChange(Intent intent) {
121         final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE,
122                 isMobileDataEnabled());
123 
124         final int defaultSubId = getDefaultSubscriptionId(mSubscriptionManager);
125         if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
126             return; // No subscription - do nothing.
127         }
128 
129         MobileNetworkUtils.setMobileDataEnabled(mContext, defaultSubId, newState,
130                 false /* disableOtherSubscriptions */);
131         // Do not notifyChange on Uri. The service takes longer to update the current value than it
132         // does for the Slice to check the current value again. Let {@link WifiScanWorker}
133         // handle it.
134     }
135 
136     @Override
getIntentFilter()137     public IntentFilter getIntentFilter() {
138         final IntentFilter filter = new IntentFilter();
139         filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
140         return filter;
141     }
142 
143     @Override
getIntent()144     public Intent getIntent() {
145         return new Intent(mContext, MobileNetworkActivity.class);
146     }
147 
148     @Override
getSliceHighlightMenuRes()149     public int getSliceHighlightMenuRes() {
150         return R.string.menu_key_network;
151     }
152 
153     @Override
getBackgroundWorkerClass()154     public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() {
155         return MobileDataWorker.class;
156     }
157 
getDefaultSubscriptionId(SubscriptionManager subscriptionManager)158     protected static int getDefaultSubscriptionId(SubscriptionManager subscriptionManager) {
159         final SubscriptionInfo defaultSubscription = subscriptionManager.getActiveSubscriptionInfo(
160                 subscriptionManager.getDefaultDataSubscriptionId());
161         if (defaultSubscription == null) {
162             return SubscriptionManager.INVALID_SUBSCRIPTION_ID; // No default subscription
163         }
164 
165         return defaultSubscription.getSubscriptionId();
166     }
167 
getSummary()168     private CharSequence getSummary() {
169         final SubscriptionInfo defaultSubscription = mSubscriptionManager.getActiveSubscriptionInfo(
170                 mSubscriptionManager.getDefaultDataSubscriptionId());
171         if (defaultSubscription == null) {
172             return null; // no summary text
173         }
174 
175         return SubscriptionUtil.getUniqueSubscriptionDisplayName(defaultSubscription, mContext);
176     }
177 
getPrimaryAction()178     private PendingIntent getPrimaryAction() {
179         final Intent intent = getIntent();
180         return PendingIntent.getActivity(mContext, 0 /* requestCode */, intent,
181                 PendingIntent.FLAG_IMMUTABLE);
182     }
183 
184     /**
185      * @return {@code true} when mobile data is not supported by the current device.
186      */
isMobileDataAvailable()187     private boolean isMobileDataAvailable() {
188         final List<SubscriptionInfo> subInfoList =
189                 SubscriptionUtil.getSelectableSubscriptionInfoList(mContext);
190 
191         return !(subInfoList == null || subInfoList.isEmpty());
192     }
193 
194     @VisibleForTesting
isAirplaneModeEnabled()195     boolean isAirplaneModeEnabled() {
196         return WirelessUtils.isAirplaneModeOn(mContext);
197     }
198 
199     @VisibleForTesting
isMobileDataEnabled()200     boolean isMobileDataEnabled() {
201         if (mTelephonyManager == null) {
202             return false;
203         }
204 
205         return mTelephonyManager.isDataEnabled();
206     }
207 
208     /**
209      * Listener for mobile data state changes.
210      *
211      * <p>
212      *     Listen to individual subscription changes since there is no framework broadcast.
213      *
214      *     This worker registers a ContentObserver in the background and updates the MobileData
215      *     Slice when the value changes.
216      */
217     public static class MobileDataWorker extends SliceBackgroundWorker<Void> {
218 
219         DataContentObserver mMobileDataObserver;
220 
MobileDataWorker(Context context, Uri uri)221         public MobileDataWorker(Context context, Uri uri) {
222             super(context, uri);
223             final Handler handler = new Handler(Looper.getMainLooper());
224             mMobileDataObserver = new DataContentObserver(handler, this);
225         }
226 
227         @Override
onSlicePinned()228         protected void onSlicePinned() {
229             final SubscriptionManager subscriptionManager =
230                     getContext().getSystemService(SubscriptionManager.class);
231             mMobileDataObserver.register(getContext(),
232                     getDefaultSubscriptionId(subscriptionManager));
233         }
234 
235         @Override
onSliceUnpinned()236         protected void onSliceUnpinned() {
237             mMobileDataObserver.unRegister(getContext());
238         }
239 
240         @Override
close()241         public void close() throws IOException {
242             mMobileDataObserver = null;
243         }
244 
updateSlice()245         public void updateSlice() {
246             notifySliceChange();
247         }
248 
249         public class DataContentObserver extends ContentObserver {
250 
251             private final MobileDataWorker mSliceBackgroundWorker;
252 
DataContentObserver(Handler handler, MobileDataWorker backgroundWorker)253             public DataContentObserver(Handler handler, MobileDataWorker backgroundWorker) {
254                 super(handler);
255                 mSliceBackgroundWorker = backgroundWorker;
256             }
257 
258             @Override
onChange(boolean selfChange)259             public void onChange(boolean selfChange) {
260                 mSliceBackgroundWorker.updateSlice();
261             }
262 
register(Context context, int subId)263             public void register(Context context, int subId) {
264                 final Uri uri = MobileDataContentObserver.getObservableUri(context, subId);
265                 context.getContentResolver().registerContentObserver(uri, false, this);
266             }
267 
unRegister(Context context)268             public void unRegister(Context context) {
269                 context.getContentResolver().unregisterContentObserver(this);
270             }
271         }
272     }
273 }
274