diff --git a/res/values/strings.xml b/res/values/strings.xml index 061164c04be..3f3c0f33d83 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12490,6 +12490,10 @@ Connect to public networks network connection, internet, wireless, data, wifi, wi-fi, wi fi, cellular, mobile, cell carrier, 4g, 3g, 2g, lte + + View airplane-safe networks + + Turn off Airplane Mode Unavailable because bedtime mode is on diff --git a/src/com/android/settings/network/AirplaneSafeNetworksSlice.java b/src/com/android/settings/network/AirplaneSafeNetworksSlice.java new file mode 100644 index 00000000000..fbef28273e3 --- /dev/null +++ b/src/com/android/settings/network/AirplaneSafeNetworksSlice.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 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; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.util.Log; + +import androidx.annotation.IntDef; +import androidx.core.graphics.drawable.IconCompat; +import androidx.slice.Slice; +import androidx.slice.builders.ListBuilder; +import androidx.slice.builders.ListBuilder.RowBuilder; +import androidx.slice.builders.SliceAction; + +import com.android.settings.AirplaneModeEnabler; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.slices.CustomSliceRegistry; +import com.android.settings.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settings.slices.SliceBroadcastReceiver; +import com.android.settingslib.WirelessUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * {@link CustomSliceable} for airplane-safe networks, used by generic clients. + */ +// TODO(b/173413889): Need to update the slice to Button style. +public class AirplaneSafeNetworksSlice implements CustomSliceable, + AirplaneModeEnabler.OnAirplaneModeChangedListener { + + private static final String TAG = "AirplaneSafeNetworksSlice"; + + public static final String ACTION_INTENT_EXTRA = "action"; + + /** + * Annotation for different action of the slice. + * + * {@code VIEW_AIRPLANE_SAFE_NETWORKS} for action of turning on Wi-Fi. + * {@code TURN_OFF_AIRPLANE_MODE} for action of turning off Airplane Mode. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + Action.VIEW_AIRPLANE_SAFE_NETWORKS, + Action.TURN_OFF_AIRPLANE_MODE, + }) + public @interface Action { + int VIEW_AIRPLANE_SAFE_NETWORKS = 1; + int TURN_OFF_AIRPLANE_MODE = 2; + } + + private final Context mContext; + private final AirplaneModeEnabler mAirplaneModeEnabler; + private final WifiManager mWifiManager; + + public AirplaneSafeNetworksSlice(Context context) { + mContext = context; + mAirplaneModeEnabler = new AirplaneModeEnabler(context, this); + mWifiManager = mContext.getSystemService(WifiManager.class); + } + + private static void logd(String s) { + Log.d(TAG, s); + } + + @Override + public Slice getSlice() { + if (!WirelessUtils.isAirplaneModeOn(mContext)) { + return null; + } + + return new ListBuilder(mContext, getUri(), ListBuilder.INFINITY) + .addRow(new RowBuilder() + .setTitle(getTitle()) + .setPrimaryAction(getSliceAction())) + .build(); + } + + @Override + public Uri getUri() { + return CustomSliceRegistry.AIRPLANE_SAFE_NETWORKS_SLICE_URI; + } + + @Override + public void onNotifyChange(Intent intent) { + final int action = intent.getIntExtra(ACTION_INTENT_EXTRA, 0); + if (action == Action.VIEW_AIRPLANE_SAFE_NETWORKS) { + if (!mWifiManager.isWifiEnabled()) { + logd("Action: turn on WiFi"); + mWifiManager.setWifiEnabled(true); + } + } else if (action == Action.TURN_OFF_AIRPLANE_MODE) { + if (WirelessUtils.isAirplaneModeOn(mContext)) { + logd("Action: turn off Airplane mode"); + mAirplaneModeEnabler.setAirplaneMode(false); + } + } + } + + @Override + public void onAirplaneModeChanged(boolean isAirplaneModeOn) { + final AirplaneSafeNetworksWorker worker = SliceBackgroundWorker.getInstance(getUri()); + if (worker != null) { + worker.updateSlice(); + } + } + + @Override + public Intent getIntent() { + return new Intent(getUri().toString()) + .setData(getUri()) + .setClass(mContext, SliceBroadcastReceiver.class) + .putExtra(ACTION_INTENT_EXTRA, getAction()); + } + + @Action + private int getAction() { + return mWifiManager.isWifiEnabled() + ? Action.TURN_OFF_AIRPLANE_MODE + : Action.VIEW_AIRPLANE_SAFE_NETWORKS; + } + + private String getTitle() { + return mContext.getText( + (getAction() == Action.VIEW_AIRPLANE_SAFE_NETWORKS) + ? R.string.view_airplane_safe_networks + : R.string.turn_off_airplane_mode).toString(); + } + + private SliceAction getSliceAction() { + final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, + 0 /* requestCode */, getIntent(), + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + final IconCompat icon = Utils.createIconWithDrawable(new ColorDrawable(Color.TRANSPARENT)); + return SliceAction.createDeeplink(pendingIntent, icon, ListBuilder.ACTION_WITH_LABEL, + getTitle()); + } + + @Override + public Class getBackgroundWorkerClass() { + return AirplaneSafeNetworksWorker.class; + } + + public static class AirplaneSafeNetworksWorker extends SliceBackgroundWorker { + + private final IntentFilter mIntentFilter; + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) { + notifySliceChange(); + } + } + }; + + public AirplaneSafeNetworksWorker(Context context, Uri uri) { + super(context, uri); + mIntentFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION); + } + + @Override + protected void onSlicePinned() { + getContext().registerReceiver(mBroadcastReceiver, mIntentFilter); + } + + @Override + protected void onSliceUnpinned() { + getContext().unregisterReceiver(mBroadcastReceiver); + } + + @Override + public void close() { + // Do nothing. + } + + public void updateSlice() { + notifySliceChange(); + } + } +} diff --git a/src/com/android/settings/panel/InternetConnectivityPanel.java b/src/com/android/settings/panel/InternetConnectivityPanel.java index db0c5e33fcc..6ae70895ee3 100644 --- a/src/com/android/settings/panel/InternetConnectivityPanel.java +++ b/src/com/android/settings/panel/InternetConnectivityPanel.java @@ -23,6 +23,7 @@ import android.net.Uri; import android.provider.Settings; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.network.AirplaneModePreferenceController; import com.android.settings.slices.CustomSliceRegistry; @@ -58,7 +59,11 @@ public class InternetConnectivityPanel implements PanelContent { final List uris = new ArrayList<>(); uris.add(CustomSliceRegistry.WIFI_SLICE_URI); uris.add(CustomSliceRegistry.MOBILE_DATA_SLICE_URI); - uris.add(AirplaneModePreferenceController.SLICE_URI); + if (Utils.isProviderModelEnabled(mContext)) { + uris.add(CustomSliceRegistry.AIRPLANE_SAFE_NETWORKS_SLICE_URI); + } else { + uris.add(AirplaneModePreferenceController.SLICE_URI); + } return uris; } diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index 30079538663..ce98d271830 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -41,6 +41,7 @@ import com.android.settings.media.MediaOutputGroupSlice; import com.android.settings.media.MediaOutputIndicatorSlice; import com.android.settings.media.MediaOutputSlice; import com.android.settings.media.RemoteMediaSlice; +import com.android.settings.network.AirplaneSafeNetworksSlice; import com.android.settings.network.telephony.MobileDataSlice; import com.android.settings.notification.zen.ZenModeButtonPreferenceController; import com.android.settings.wifi.calling.WifiCallingSliceHelper; @@ -314,6 +315,16 @@ public class CustomSliceRegistry { .appendPath("always_on_display") .build(); + /** + * Backing Uri for the Always On Slice. + */ + public static final Uri AIRPLANE_SAFE_NETWORKS_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath("airplane_safe_networks") + .build(); + @VisibleForTesting static final Map> sUriToSlice; @@ -336,6 +347,7 @@ public class CustomSliceRegistry { sUriToSlice.put(REMOTE_MEDIA_SLICE_URI, RemoteMediaSlice.class); sUriToSlice.put(MEDIA_OUTPUT_GROUP_SLICE_URI, MediaOutputGroupSlice.class); sUriToSlice.put(ALWAYS_ON_SLICE_URI, AlwaysOnDisplaySlice.class); + sUriToSlice.put(AIRPLANE_SAFE_NETWORKS_SLICE_URI, AirplaneSafeNetworksSlice.class); } public static Class getSliceClassByUri(Uri uri) { diff --git a/tests/unit/src/com/android/settings/network/AirplaneSafeNetworksSliceTest.java b/tests/unit/src/com/android/settings/network/AirplaneSafeNetworksSliceTest.java new file mode 100644 index 00000000000..ce47eb7d6dd --- /dev/null +++ b/tests/unit/src/com/android/settings/network/AirplaneSafeNetworksSliceTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2020 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiManager; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.widget.SliceLiveData; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.AirplaneModeEnabler; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(AndroidJUnit4.class) +public class AirplaneSafeNetworksSliceTest { + + private static final String VIEW_AIRPLANE_SAFE_NETWORKS = "View airplane-safe networks"; + private static final String TURN_OFF_AIRPLANE_MODE = "Turn off Airplane Mode"; + + @Rule + public MockitoRule mMocks = MockitoJUnit.rule(); + @Mock + private WifiManager mWifiManager; + + private Context mContext; + private AirplaneModeEnabler mAirplaneModeEnabler; + private AirplaneSafeNetworksSlice mAirplaneSafeNetworksSlice; + + @Before + public void setUp() { + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifiManager); + mAirplaneModeEnabler = + new AirplaneModeEnabler(mContext, null /* OnAirplaneModeChangedListener */); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mAirplaneSafeNetworksSlice = new AirplaneSafeNetworksSlice(mContext); + } + + @Test + public void getSlice_airplaneModeOff_shouldBeNull() { + mAirplaneModeEnabler.setAirplaneMode(false); + + assertThat(mAirplaneSafeNetworksSlice.getSlice()).isNull(); + } + + @Test + public void getSlice_wifiDisabled_shouldShowViewAirplaneSafeNetworks() { + mAirplaneModeEnabler.setAirplaneMode(true); + when(mWifiManager.isWifiEnabled()).thenReturn(false); + + final Slice slice = mAirplaneSafeNetworksSlice.getSlice(); + + assertThat(slice).isNotNull(); + final SliceItem sliceTitle = + SliceMetadata.from(mContext, slice).getListContent().getHeader().getTitleItem(); + assertThat(sliceTitle.getText()).isEqualTo(VIEW_AIRPLANE_SAFE_NETWORKS); + } + + @Test + public void getSlice_wifiEnabled_shouldShowTurnOffAirplaneMode() { + mAirplaneModeEnabler.setAirplaneMode(true); + when(mWifiManager.isWifiEnabled()).thenReturn(true); + + final Slice slice = mAirplaneSafeNetworksSlice.getSlice(); + + assertThat(slice).isNotNull(); + final SliceItem sliceTitle = + SliceMetadata.from(mContext, slice).getListContent().getHeader().getTitleItem(); + assertThat(sliceTitle.getText()).isEqualTo(TURN_OFF_AIRPLANE_MODE); + } + + @Test + public void onNotifyChange_viewAirplaneSafeNetworks_shouldSetWifiEnabled() { + mAirplaneModeEnabler.setAirplaneMode(true); + when(mWifiManager.isWifiEnabled()).thenReturn(false); + Intent intent = mAirplaneSafeNetworksSlice.getIntent(); + + mAirplaneSafeNetworksSlice.onNotifyChange(intent); + + verify(mWifiManager).setWifiEnabled(true); + } + + @Test + public void onNotifyChange_turnOffAirplaneMode_shouldSetAirplaneModeOff() { + mAirplaneModeEnabler.setAirplaneMode(true); + when(mWifiManager.isWifiEnabled()).thenReturn(true); + Intent intent = mAirplaneSafeNetworksSlice.getIntent(); + + mAirplaneSafeNetworksSlice.onNotifyChange(intent); + + assertThat(mAirplaneModeEnabler.isAirplaneModeOn()).isFalse(); + } +}