Merge "Create special case mobile data slice"

This commit is contained in:
TreeHugger Robot
2019-02-11 22:53:50 +00:00
committed by Android (Google) Code Review
13 changed files with 463 additions and 19 deletions

View File

@@ -2991,7 +2991,7 @@
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" />

View File

@@ -119,6 +119,14 @@ public class MobileDataPreferenceController extends TelephonyTogglePreferenceCon
&& 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;
@@ -169,10 +177,7 @@ public class MobileDataPreferenceController extends TelephonyTogglePreferenceCon
}
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);
}

View File

@@ -0,0 +1,249 @@
/*
* 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);
}
}
}
}

View File

@@ -55,6 +55,7 @@ public class InternetConnectivityPanel implements PanelContent {
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;
}

View File

@@ -18,7 +18,6 @@ package com.android.settings.slices;
import android.content.Context;
import android.net.Uri;
import android.text.TextUtils;
import android.util.ArrayMap;
import androidx.annotation.VisibleForTesting;
@@ -35,6 +34,7 @@ import com.android.settings.homepage.contextualcards.slices.LowStorageSlice;
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;
@@ -123,6 +123,7 @@ public class CustomSliceManager {
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);

View File

@@ -164,6 +164,16 @@ public class CustomSliceRegistry {
.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.
*/

View File

@@ -71,7 +71,7 @@ public interface CustomSliceable {
*
* @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.
@@ -90,11 +90,12 @@ public interface CustomSliceable {
}
/**
* 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;

View File

@@ -464,8 +464,9 @@ public class SettingsSliceProvider extends SliceProvider {
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
);
}

View File

@@ -59,6 +59,14 @@ public abstract class SliceBackgroundWorker<E> implements Closeable {
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
@@ -151,7 +159,7 @@ public abstract class SliceBackgroundWorker<E> implements Closeable {
/**
* 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);
}
}

View File

@@ -142,7 +142,7 @@ public class WifiSlice implements CustomSliceable {
.setTitle(title)
.setSubtitle(!TextUtils.isEmpty(apSummary)
? apSummary
: mContext.getText(R.string.summary_placeholder))
: null)
.setPrimaryAction(SliceAction.create(
getAccessPointAction(accessPoint), levelIcon, ListBuilder.ICON_IMAGE,
title));
@@ -247,7 +247,7 @@ public class WifiSlice implements CustomSliceable {
return mContext.getText(R.string.switch_off_text);
case WifiManager.WIFI_STATE_UNKNOWN:
default:
return "";
return null;
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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);
}
}

View File

@@ -46,8 +46,10 @@ public class InternetConnectivityPanelTest {
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

View File

@@ -116,7 +116,8 @@ public class SettingsSliceProviderTest {
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