android:label="@string/settings_panel_title"
android:theme="@style/Theme.BottomDialog"
android:excludeFromRecents="true"
- android:launchMode="singleTask"
+ android:launchMode="singleTop"
android:exported="true">
<intent-filter>
<action android:name="android.settings.panel.action.INTERNET_CONNECTIVITY" />
&& mSubId == SubscriptionManager.getDefaultDataSubscriptionId();
}
+ public static Uri getObservableUri(int subId) {
+ Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA);
+ if (TelephonyManager.getDefault().getSimCount() != 1) {
+ uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + subId);
+ }
+ return uri;
+ }
+
public void init(FragmentManager fragmentManager, int subId) {
mFragmentManager = fragmentManager;
mSubId = subId;
}
public void register(Context context, int subId) {
- Uri uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA);
- if (TelephonyManager.getDefault().getSimCount() != 1) {
- uri = Settings.Global.getUriFor(Settings.Global.MOBILE_DATA + subId);
- }
+ final Uri uri = getObservableUri(subId);
context.getContentResolver().registerContentObserver(uri, false, this);
}
--- /dev/null
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.settings.network.telephony;
+
+import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+
+import android.annotation.ColorInt;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.slice.Slice;
+import androidx.slice.builders.ListBuilder;
+import androidx.slice.builders.SliceAction;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.network.AirplaneModePreferenceController;
+import com.android.settings.slices.CustomSliceRegistry;
+import com.android.settings.slices.CustomSliceable;
+import com.android.settings.slices.SliceBackgroundWorker;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.IOException;
+
+/**
+ * Custom {@link Slice} for Mobile Data.
+ * <p>
+ * We make a custom slice instead of using {@link MobileDataPreferenceController} because the
+ * pref controller is generalized across any carrier, and thus does not control a specific
+ * subscription. We attempt to reuse any telephony-specific code from the preference controller.
+ *
+ * </p>
+ *
+ */
+public class MobileDataSlice implements CustomSliceable {
+
+ private final Context mContext;
+ private final SubscriptionManager mSubscriptionManager;
+ private final TelephonyManager mTelephonyManager;
+
+ public MobileDataSlice(Context context) {
+ mContext = context;
+ mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
+ mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
+ }
+
+ @Override
+ public Slice getSlice() {
+ final IconCompat icon = IconCompat.createWithResource(mContext,
+ R.drawable.ic_network_cell);
+ final String title = mContext.getText(R.string.mobile_data_settings_title).toString();
+ final CharSequence summary = getSummary();
+ @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext);
+ final PendingIntent toggleAction = getBroadcastIntent(mContext);
+ final PendingIntent primaryAction = getPrimaryAction();
+ final SliceAction primarySliceAction = SliceAction.createDeeplink(primaryAction, icon,
+ ListBuilder.ICON_IMAGE, title);
+ final SliceAction toggleSliceAction = SliceAction.createToggle(toggleAction,
+ null /* actionTitle */, isMobileDataEnabled());
+
+ final ListBuilder listBuilder = new ListBuilder(mContext, getUri(),
+ ListBuilder.INFINITY)
+ .setAccentColor(color)
+ .addRow(new ListBuilder.RowBuilder()
+ .setTitle(title)
+ .setSubtitle(summary)
+ .addEndItem(toggleSliceAction)
+ .setPrimaryAction(primarySliceAction));
+ return listBuilder.build();
+ }
+
+ @Override
+ public Uri getUri() {
+ return CustomSliceRegistry.MOBILE_DATA_SLICE_URI;
+ }
+
+ @Override
+ public void onNotifyChange(Intent intent) {
+ // Don't make a change if we are in Airplane Mode.
+ if (isAirplaneModeEnabled()) {
+ return;
+ }
+
+ final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE,
+ isMobileDataEnabled());
+
+ final int defaultSubId = getDefaultSubscriptionId(mSubscriptionManager);
+ if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ return; // No subscription - do nothing.
+ }
+
+ MobileNetworkUtils.setMobileDataEnabled(mContext, defaultSubId, newState,
+ false /* disableOtherSubscriptions */);
+ // Do not notifyChange on Uri. The service takes longer to update the current value than it
+ // does for the Slice to check the current value again. Let {@link WifiScanWorker}
+ // handle it.
+ }
+
+ @Override
+ public IntentFilter getIntentFilter() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ return filter;
+ }
+
+ @Override
+ public Intent getIntent() {
+ return new Intent(mContext, MobileNetworkActivity.class);
+ }
+
+ @Override
+ public Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() {
+ return MobileDataWorker.class;
+ }
+
+ protected static int getDefaultSubscriptionId(SubscriptionManager subscriptionManager) {
+ final SubscriptionInfo defaultSubscription =
+ subscriptionManager.getDefaultDataSubscriptionInfo();
+ if (defaultSubscription == null) {
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID; // No default subscription
+ }
+
+ return defaultSubscription.getSubscriptionId();
+ }
+
+ private CharSequence getSummary() {
+ final SubscriptionInfo defaultSubscription =
+ mSubscriptionManager.getDefaultDataSubscriptionInfo();
+ if (defaultSubscription == null) {
+ return null; // no summary text
+ }
+
+ return defaultSubscription.getDisplayName();
+ }
+
+ private PendingIntent getPrimaryAction() {
+ final Intent intent = getIntent();
+ return PendingIntent.getActivity(mContext, 0 /* requestCode */,
+ intent, 0 /* flags */);
+ }
+
+ @VisibleForTesting
+ boolean isAirplaneModeEnabled() {
+ // Generic key since we only want the method check - no UI.
+ AirplaneModePreferenceController controller = new AirplaneModePreferenceController(mContext,
+ "key" /* Key */);
+ return controller.isChecked();
+ }
+
+ @VisibleForTesting
+ boolean isMobileDataEnabled() {
+ if (mTelephonyManager == null) {
+ return false;
+ }
+
+ return mTelephonyManager.isDataEnabled();
+ }
+
+ /**
+ * Listener for mobile data state changes.
+ *
+ * <p>
+ * Listen to individual subscription changes since there is no framework broadcast.
+ *
+ * This worker registers a ContentObserver in the background and updates the MobileData
+ * Slice when the value changes.
+ */
+ public static class MobileDataWorker extends SliceBackgroundWorker<Void> {
+
+ DataContentObserver mMobileDataObserver;
+
+ public MobileDataWorker(Context context, Uri uri) {
+ super(context, uri);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ mMobileDataObserver = new DataContentObserver(handler, this);
+ }
+
+ @Override
+ protected void onSlicePinned() {
+ final SubscriptionManager subscriptionManager =
+ getContext().getSystemService(SubscriptionManager.class);
+ mMobileDataObserver.register(getContext(),
+ getDefaultSubscriptionId(subscriptionManager));
+ }
+
+ @Override
+ protected void onSliceUnpinned() {
+ mMobileDataObserver.unRegister(getContext());
+ }
+
+ @Override
+ public void close() throws IOException {
+ mMobileDataObserver = null;
+ }
+
+ public void updateSlice() {
+ notifySliceChange();
+ }
+
+ public class DataContentObserver extends ContentObserver {
+
+ private final MobileDataWorker mSliceBackgroundWorker;
+
+ public DataContentObserver(Handler handler, MobileDataWorker backgroundWorker) {
+ super(handler);
+ mSliceBackgroundWorker = backgroundWorker;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mSliceBackgroundWorker.updateSlice();
+ }
+
+ public void register(Context context, int subId) {
+ final Uri uri = MobileDataPreferenceController.getObservableUri(subId);
+ context.getContentResolver().registerContentObserver(uri, false, this);
+ }
+
+ public void unRegister(Context context) {
+ context.getContentResolver().unregisterContentObserver(this);
+ }
+ }
+ }
+}
public List<Uri> getSlices() {
final List<Uri> uris = new ArrayList<>();
uris.add(CustomSliceRegistry.WIFI_SLICE_URI);
+ uris.add(CustomSliceRegistry.MOBILE_DATA_SLICE_URI);
uris.add(CustomSliceRegistry.AIRPLANE_URI);
return uris;
}
import android.content.Context;
import android.net.Uri;
-import android.text.TextUtils;
import android.util.ArrayMap;
import androidx.annotation.VisibleForTesting;
import com.android.settings.homepage.contextualcards.slices.NotificationChannelSlice;
import com.android.settings.location.LocationSlice;
import com.android.settings.media.MediaOutputSlice;
+import com.android.settings.network.telephony.MobileDataSlice;
import com.android.settings.wifi.slice.ContextualWifiSlice;
import com.android.settings.wifi.slice.WifiSlice;
mUriMap.put(CustomSliceRegistry.FLASHLIGHT_SLICE_URI, FlashlightSlice.class);
mUriMap.put(CustomSliceRegistry.LOCATION_SLICE_URI, LocationSlice.class);
mUriMap.put(CustomSliceRegistry.LOW_STORAGE_SLICE_URI, LowStorageSlice.class);
+ mUriMap.put(CustomSliceRegistry.MOBILE_DATA_SLICE_URI, MobileDataSlice.class);
mUriMap.put(CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI,
NotificationChannelSlice.class);
mUriMap.put(CustomSliceRegistry.STORAGE_SLICE_URI, StorageSlice.class);
.appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
.appendPath("toggle_nfc")
.build();
+
+ /**
+ * Backing Uri for Mobile Data Slice.
+ */
+ public static final Uri MOBILE_DATA_SLICE_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendEncodedPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath("mobile_data")
+ .build();
/**
* Backing Uri for Notification channel Slice.
*/
*
* @param intent which has the action taken on a {@link Slice}.
*/
- void onNotifyChange(Intent intent);
+ default void onNotifyChange(Intent intent) {}
/**
* @return an {@link Intent} to the source of the Slice data.
}
/**
- * Settings Slices which can represent component lists that are updatable by the
- * {@link SliceBackgroundWorker} class returned here.
+ * Settings Slices which require background work, such as updating lists should implement a
+ * {@link SliceBackgroundWorker} and return it here. An example of background work is updating
+ * a list of Wifi networks available in the area.
*
- * @return a {@link SliceBackgroundWorker} class for fetching the list of results in the
- * background.
+ * @return a {@link Class<? extends SliceBackgroundWorker>} to perform background work for the
+ * slice.
*/
default Class<? extends SliceBackgroundWorker> getBackgroundWorkerClass() {
return null;
private List<Uri> getSpecialCaseOemUris() {
return Arrays.asList(
- CustomSliceRegistry.ZEN_MODE_SLICE_URI,
- CustomSliceRegistry.FLASHLIGHT_SLICE_URI
+ CustomSliceRegistry.FLASHLIGHT_SLICE_URI,
+ CustomSliceRegistry.MOBILE_DATA_SLICE_URI,
+ CustomSliceRegistry.ZEN_MODE_SLICE_URI
);
}
mUri = uri;
}
+ protected Uri getUri() {
+ return mUri;
+ }
+
+ protected Context getContext() {
+ return mContext;
+ }
+
/**
* Returns the singleton instance of the {@link SliceBackgroundWorker} for specified {@link Uri}
* if exists
/**
* Notify that data was updated and attempt to sync changes to the Slice.
*/
- protected void notifySliceChange() {
+ protected final void notifySliceChange() {
mContext.getContentResolver().notifyChange(mUri, null);
}
}
\ No newline at end of file
.setTitle(title)
.setSubtitle(!TextUtils.isEmpty(apSummary)
? apSummary
- : mContext.getText(R.string.summary_placeholder))
+ : null)
.setPrimaryAction(SliceAction.create(
getAccessPointAction(accessPoint), levelIcon, ListBuilder.ICON_IMAGE,
title));
return mContext.getText(R.string.switch_off_text);
case WifiManager.WIFI_STATE_UNKNOWN:
default:
- return "";
+ return null;
}
}
--- /dev/null
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.settings.network.telephony;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.slice.Slice;
+import androidx.slice.SliceMetadata;
+import androidx.slice.SliceProvider;
+import androidx.slice.core.SliceAction;
+import androidx.slice.widget.SliceLiveData;
+
+import com.android.settings.R;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class MobileDataSliceTest {
+
+ private static final int SUB_ID = 2;
+
+ @Mock
+ private TelephonyManager mTelephonyManager;
+ @Mock
+ private SubscriptionManager mSubscriptionManager;
+ @Mock
+ private SubscriptionInfo mSubscriptionInfo;
+
+ private Context mContext;
+ private MobileDataSlice mMobileDataSlice;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+
+ doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE);
+ doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class);
+ doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(SUB_ID);
+ doReturn(mSubscriptionInfo).when(mSubscriptionManager).getDefaultDataSubscriptionInfo();
+ doReturn(SUB_ID).when(mSubscriptionInfo).getSubscriptionId();
+
+ // Set-up specs for SliceMetadata.
+ SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
+
+ mMobileDataSlice = spy(new MobileDataSlice(mContext));
+ }
+
+ @Test
+ public void getSlice_shouldHaveTitleAndToggle() {
+ final Slice mobileData = mMobileDataSlice.getSlice();
+
+ final SliceMetadata metadata = SliceMetadata.from(mContext, mobileData);
+ assertThat(metadata.getTitle())
+ .isEqualTo(mContext.getString(R.string.mobile_data_settings_title));
+
+ final List<SliceAction> toggles = metadata.getToggles();
+ assertThat(toggles).hasSize(1);
+
+ final SliceAction primaryAction = metadata.getPrimaryAction();
+ final IconCompat expectedToggleIcon = IconCompat.createWithResource(mContext,
+ R.drawable.ic_network_cell);
+ assertThat(primaryAction.getIcon().toString()).isEqualTo(expectedToggleIcon.toString());
+ }
+
+ @Test
+ public void handleUriChange_turnedOn_updatesMobileData() {
+ doReturn(false).when(mMobileDataSlice).isAirplaneModeEnabled();
+ doReturn(mSubscriptionInfo).when(mSubscriptionManager).getActiveSubscriptionInfo(SUB_ID);
+ final Intent intent = mMobileDataSlice.getIntent();
+ intent.putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, true);
+
+ mMobileDataSlice.onNotifyChange(intent);
+
+ verify(mTelephonyManager).setDataEnabled(true);
+ }
+
+ @Test
+ public void handleUriChange_turnedOff_updatesMobileData() {
+ doReturn(false).when(mMobileDataSlice).isAirplaneModeEnabled();
+ doReturn(mSubscriptionInfo).when(mSubscriptionManager).getActiveSubscriptionInfo(SUB_ID);
+ final Intent intent = mMobileDataSlice.getIntent();
+ intent.putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, false);
+
+ mMobileDataSlice.onNotifyChange(intent);
+
+ verify(mTelephonyManager).setDataEnabled(false);
+ }
+
+ @Test
+ public void handleUriChange_turnedOff_airplaneModeOn_mobileDataDoesNotUpdate() {
+ doReturn(true).when(mMobileDataSlice).isAirplaneModeEnabled();
+ doReturn(mSubscriptionInfo).when(mSubscriptionManager).getActiveSubscriptionInfo(SUB_ID);
+ final Intent intent = mMobileDataSlice.getIntent();
+ intent.putExtra(android.app.slice.Slice.EXTRA_TOGGLE_STATE, false);
+
+ mMobileDataSlice.onNotifyChange(intent);
+
+ verify(mTelephonyManager, times(0)).setDataEnabled(true);
+ }
+
+ @Test
+ public void isAirplaneModeEnabled_correctlyReturnsTrue() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+
+ final boolean isAirplaneModeEnabled = mMobileDataSlice.isAirplaneModeEnabled();
+
+ assertThat(isAirplaneModeEnabled).isTrue();
+ }
+
+ @Test
+ public void isAirplaneModeEnabled_correctlyReturnsFalse() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
+ final boolean isAirplaneModeEnabled = mMobileDataSlice.isAirplaneModeEnabled();
+
+ assertThat(isAirplaneModeEnabled).isFalse();
+ }
+
+ @Test
+ public void isMobileDataEnabled_mobileDataEnabled() {
+ final boolean seed = true;
+ doReturn(seed).when(mTelephonyManager).isDataEnabled();
+
+ final boolean isMobileDataEnabled = mMobileDataSlice.isMobileDataEnabled();
+
+ assertThat(isMobileDataEnabled).isEqualTo(seed);
+ }
+}
public void getSlices_containsNecessarySlices() {
final List<Uri> uris = mPanel.getSlices();
- assertThat(uris).containsExactly(CustomSliceRegistry.WIFI_SLICE_URI,
- CustomSliceRegistry.AIRPLANE_URI);
+ assertThat(uris).containsExactly(
+ CustomSliceRegistry.AIRPLANE_URI,
+ CustomSliceRegistry.MOBILE_DATA_SLICE_URI,
+ CustomSliceRegistry.WIFI_SLICE_URI);
}
@Test
private static final List<Uri> SPECIAL_CASE_OEM_URIS = Arrays.asList(
CustomSliceRegistry.ZEN_MODE_SLICE_URI,
- CustomSliceRegistry.FLASHLIGHT_SLICE_URI
+ CustomSliceRegistry.FLASHLIGHT_SLICE_URI,
+ CustomSliceRegistry.MOBILE_DATA_SLICE_URI
);
@Before