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