From 88ad3307ebc00e42379c7e90e61acaf8bbfd531d Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Tue, 19 Feb 2019 18:14:33 +0800 Subject: [PATCH 01/13] Add Grayscale conditional Build Grayscale conditional that lets users quickly turn off Grayscale in Settings if it is on. Bug: 118387886 Test: visual, robotests Change-Id: Ibfc2d88f4f3f60f9b0acf084a49084030674de37 --- res/drawable/ic_gray_scale_24dp.xml | 25 +++++ res/values/config.xml | 3 + res/values/strings.xml | 6 + .../conditional/ConditionManager.java | 1 + .../GrayscaleConditionController.java | 103 ++++++++++++++++++ tests/robotests/res/values-mcc999/config.xml | 3 + .../GrayscaleConditionControllerTest.java | 83 ++++++++++++++ 7 files changed, 224 insertions(+) create mode 100644 res/drawable/ic_gray_scale_24dp.xml create mode 100644 src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java create mode 100644 tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java diff --git a/res/drawable/ic_gray_scale_24dp.xml b/res/drawable/ic_gray_scale_24dp.xml new file mode 100644 index 00000000000..3fda134ade7 --- /dev/null +++ b/res/drawable/ic_gray_scale_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/res/values/config.xml b/res/values/config.xml index e6ada66a442..805469a63e5 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -356,4 +356,7 @@ content://com.google.android.gms.nearby.fastpair/device_status_list_item + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 0d6eaeb2527..2e1f4c7171e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9275,6 +9275,12 @@ Screen tinted amber + + Greyscale + + + Display only in grey color + %1$d diff --git a/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java index c741b98c359..66f6c81e984 100644 --- a/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java +++ b/src/com/android/settings/homepage/contextualcards/conditional/ConditionManager.java @@ -162,6 +162,7 @@ public class ConditionManager { mCardControllers.add(new RingerVibrateConditionController(mAppContext, this /* manager */)); mCardControllers.add(new RingerMutedConditionController(mAppContext, this /* manager */)); mCardControllers.add(new WorkModeConditionController(mAppContext, this /* manager */)); + mCardControllers.add(new GrayscaleConditionController(mAppContext, this /* manager */)); } /** diff --git a/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java new file mode 100644 index 00000000000..664707def69 --- /dev/null +++ b/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionController.java @@ -0,0 +1,103 @@ +/* + * 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.homepage.contextualcards.conditional; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.ColorDisplayManager; +import android.util.Log; + +import com.android.settings.R; +import com.android.settings.homepage.contextualcards.ContextualCard; + +import java.net.URISyntaxException; +import java.util.Objects; + +public class GrayscaleConditionController implements ConditionalCardController { + static final int ID = Objects.hash("GrayscaleConditionController"); + + private static final String TAG = "GrayscaleCondition"; + + private final Context mAppContext; + private final ConditionManager mConditionManager; + private final ColorDisplayManager mColorDisplayManager; + + private Intent mIntent; + + public GrayscaleConditionController(Context appContext, ConditionManager conditionManager) { + mAppContext = appContext; + mConditionManager = conditionManager; + mColorDisplayManager = mAppContext.getSystemService(ColorDisplayManager.class); + } + + @Override + public long getId() { + return ID; + } + + @Override + public boolean isDisplayable() { + try { + mIntent = Intent.parseUri( + mAppContext.getString(R.string.config_grayscale_settings_intent), + Intent.URI_INTENT_SCHEME); + } catch (URISyntaxException e) { + Log.w(TAG, "Failure parsing grayscale settings intent, skipping", e); + return false; + } + return mColorDisplayManager.isSaturationActivated(); + } + + @Override + public void onPrimaryClick(Context context) { + mAppContext.startActivity(mIntent); + } + + @Override + public void onActionClick() { + // Turn off grayscale + mColorDisplayManager.setSaturationLevel(100 /* staturationLevel */); + mConditionManager.onConditionChanged(); + } + + @Override + public ContextualCard buildContextualCard() { + return new ConditionalContextualCard.Builder() + .setConditionId(ID) + .setMetricsConstant(SettingsEnums.SETTINGS_CONDITION_GRAYSCALE_MODE) + .setActionText(mAppContext.getText(R.string.condition_turn_off)) + .setName(mAppContext.getPackageName() + "/" + mAppContext.getText( + R.string.condition_grayscale_title)) + .setTitleText(mAppContext.getText(R.string.condition_grayscale_title).toString()) + .setSummaryText( + mAppContext.getText(R.string.condition_grayscale_summary).toString()) + .setIconDrawable(mAppContext.getDrawable(R.drawable.ic_gray_scale_24dp)) + .setViewType(ConditionContextualCardRenderer.VIEW_TYPE_HALF_WIDTH) + .build(); + } + + @Override + public void startMonitoringStateChange() { + + } + + @Override + public void stopMonitoringStateChange() { + + } +} diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index 776a4d0902f..1fe4bbeecf0 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -91,4 +91,7 @@ test@test.test + + + intent:#Intent;action=test.test;end diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java new file mode 100644 index 00000000000..8c24735c77a --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/contextualcards/conditional/GrayscaleConditionControllerTest.java @@ -0,0 +1,83 @@ +/* + * 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.homepage.contextualcards.conditional; + +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.verify; + +import android.content.Context; +import android.hardware.display.ColorDisplayManager; + +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 org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +public class GrayscaleConditionControllerTest { + + @Mock + private ConditionManager mConditionManager; + + private ColorDisplayManager mColorDisplayManager; + private Context mContext; + private GrayscaleConditionController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mColorDisplayManager = spy(mContext.getSystemService(ColorDisplayManager.class)); + doReturn(mColorDisplayManager).when(mContext).getSystemService(ColorDisplayManager.class); + mController = new GrayscaleConditionController(mContext, mConditionManager); + } + + @Test + public void isDisplayable_noIntent_shouldReturnFalse() { + assertThat(mController.isDisplayable()).isFalse(); + } + + @Test + @Config(qualifiers = "mcc999") + public void isDisplayable_validIntentAndGrayscaleOn_shouldReturnTrue() { + doReturn(true).when(mColorDisplayManager).isSaturationActivated(); + + assertThat(mController.isDisplayable()).isTrue(); + } + + @Test + @Config(qualifiers = "mcc999") + public void isDisplayable_validIntentAndGrayscaleOff_shouldReturnFalse() { + doReturn(false).when(mColorDisplayManager).isSaturationActivated(); + + assertThat(mController.isDisplayable()).isFalse(); + } + + @Test + public void onActionClick_shouldRefreshCondition() { + mController.onActionClick(); + + verify(mConditionManager).onConditionChanged(); + } +} From 7fe8f9284667d28f16bd4aebf905f8684b573b69 Mon Sep 17 00:00:00 2001 From: clownshen Date: Mon, 11 Mar 2019 22:28:10 +0800 Subject: [PATCH 02/13] Display more information on network detail page For connected network, show all information at detail page. For disconnected network, show signal strength, Mac address, etc. Bug: 124707751 Test: atest Test: manual test Change-Id: I401621abf9bba5dc7900b47a811d595adc906c13 --- .../WifiDetailPreferenceController.java | 264 +++++++++++++----- ...etailSavedNetworkPreferenceController.java | 66 ----- .../details/WifiNetworkDetailsFragment.java | 36 +-- .../SavedAccessPointsWifiSettings.java | 1 - 4 files changed, 204 insertions(+), 163 deletions(-) delete mode 100644 src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java diff --git a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java index 89565dfb203..5588977554b 100644 --- a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java +++ b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java @@ -66,6 +66,7 @@ import com.android.settings.wifi.WifiDialog; import com.android.settings.wifi.WifiDialog.WifiDialogListener; import com.android.settings.wifi.WifiUtils; import com.android.settings.wifi.dpp.WifiDppUtils; +import com.android.settings.wifi.savedaccesspoints.SavedAccessPointsWifiSettings; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -75,6 +76,8 @@ import com.android.settingslib.core.lifecycle.events.OnResume; import com.android.settingslib.widget.ActionButtonsPreference; import com.android.settingslib.widget.LayoutPreference; import com.android.settingslib.wifi.AccessPoint; +import com.android.settingslib.wifi.WifiTracker; +import com.android.settingslib.wifi.WifiTrackerFactory; import java.net.Inet4Address; import java.net.Inet6Address; @@ -136,7 +139,9 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController private WifiConfiguration mWifiConfig; private WifiInfo mWifiInfo; private final WifiManager mWifiManager; + private final WifiTracker mWifiTracker; private final MetricsFeatureProvider mMetricsFeatureProvider; + private boolean mIsOutOfRange; // UI elements - in order of appearance private ActionButtonsPreference mButtonsPref; @@ -176,7 +181,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController // fall through case WifiManager.NETWORK_STATE_CHANGED_ACTION: case WifiManager.RSSI_CHANGED_ACTION: - updateLiveNetworkInfo(); + updateNetworkInfo(); break; } } @@ -206,14 +211,16 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController @Override public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) { // If the network just validated or lost Internet access, refresh network state. - // Don't do this on every NetworkCapabilities change because refreshNetworkState - // sends IPCs to the system server from the UI thread, which can cause jank. + // Don't do this on every NetworkCapabilities change because update accesspoint notify + // changed for accesspoint changed on the main thread, which can cause jank. if (network.equals(mNetwork) && !nc.equals(mNetworkCapabilities)) { if (hasCapabilityChanged(nc, NET_CAPABILITY_VALIDATED) || hasCapabilityChanged(nc, NET_CAPABILITY_CAPTIVE_PORTAL)) { - refreshNetworkState(); + mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + refreshEntityHeader(); } mNetworkCapabilities = nc; + refreshButtons(); updateIpLayerInfo(); } } @@ -226,6 +233,29 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController } }; + private final WifiTracker.WifiListener mWifiListener = new WifiTracker.WifiListener() { + /** Called when the state of Wifi has changed. */ + public void onWifiStateChanged(int state) { + Log.d(TAG, "onWifiStateChanged(" + state + ")"); + // Do nothing. + } + + /** Called when the connection state of wifi has changed. */ + public void onConnectedChanged() { + Log.d(TAG, "onConnectedChanged"); + // Do nothing. + } + + /** + * Called to indicate the list of AccessPoints has been updated and + * {@link WifiTracker#getAccessPoints()} should be called to get the updated list. + */ + public void onAccessPointsChanged() { + Log.d(TAG, "onAccessPointsChanged"); + updateNetworkInfo(); + } + }; + public static WifiDetailPreferenceController newInstance( AccessPoint accessPoint, ConnectivityManager connectivityManager, @@ -270,6 +300,17 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mLifecycle = lifecycle; lifecycle.addObserver(this); + + if (SavedAccessPointsWifiSettings.usingDetailsFragment(mContext)) { + mWifiTracker = WifiTrackerFactory.create( + mFragment.getActivity(), + mWifiListener, + mLifecycle, + true /*includeSaved*/, + true /*includeScans*/); + } else { + mWifiTracker = null; + } } @Override @@ -360,7 +401,7 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mNetwork = mWifiManager.getCurrentNetwork(); mLinkProperties = mConnectivityManager.getLinkProperties(mNetwork); mNetworkCapabilities = mConnectivityManager.getNetworkCapabilities(mNetwork); - updateLiveNetworkInfo(); + updateNetworkInfo(); mContext.registerReceiver(mReceiver, mFilter); mConnectivityManager.registerNetworkCallback(mNetworkRequest, mNetworkCallback, mHandler); @@ -377,72 +418,73 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); } - // TODO(b/124707751): Refactoring the code later, keeping it currently for stability. - protected void updateSavedNetworkInfo() { - mSignalStrengthPref.setVisible(false); - mFrequencyPref.setVisible(false); - mTxLinkSpeedPref.setVisible(false); - mRxLinkSpeedPref.setVisible(false); - - // MAC Address Pref - mMacAddressPref.setSummary(mWifiConfig.getRandomizedMacAddress().toString()); - - refreshEntityHeader(); - - updateIpLayerInfo(); - - // Update whether the forget button should be displayed. - mButtonsPref.setButton1Visible(canForgetNetwork()); - } - - private void updateLiveNetworkInfo() { - // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the - // callbacks. mNetwork doesn't change except in onResume. - mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); - mWifiInfo = mWifiManager.getConnectionInfo(); - if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { - exitActivity(); + private void updateNetworkInfo() { + if(!updateAccessPoint()) { return; } - // Update whether the forget button should be displayed. - mButtonsPref.setButton1Visible(canForgetNetwork()); + // refresh header + refreshEntityHeader(); - refreshNetworkState(); + // refresh Buttons + refreshButtons(); // Update Connection Header icon and Signal Strength Preference refreshRssiViews(); - - // MAC Address Pref - mMacAddressPref.setSummary(mWifiInfo.getMacAddress()); - - // Transmit Link Speed Pref - int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps(); - mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0); - mTxLinkSpeedPref.setSummary(mContext.getString( - R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps())); - - // Receive Link Speed Pref - int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps(); - mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0); - mRxLinkSpeedPref.setSummary(mContext.getString( - R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps())); - // Frequency Pref - final int frequency = mWifiInfo.getFrequency(); - String band = null; - if (frequency >= AccessPoint.LOWER_FREQ_24GHZ - && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_24ghz); - } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ - && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { - band = mContext.getResources().getString(R.string.wifi_band_5ghz); - } else { - Log.e(TAG, "Unexpected frequency " + frequency); - } - mFrequencyPref.setSummary(band); - + refreshFrequency(); + // Transmit Link Speed Pref + refreshTxSpeed(); + // Receive Link Speed Pref + refreshRxSpeed(); + // IP related information updateIpLayerInfo(); + // MAC Address Pref + refreshMacAddress(); + + } + + private boolean updateAccessPoint() { + boolean changed = false; + if (mWifiTracker != null) { + updateAccessPointFromScannedList(); + // refresh UI if signal level changed for disconnect network. + changed = mRssiSignalLevel != mAccessPoint.getLevel(); + } + + if (mAccessPoint.isActive()) { + // No need to fetch LinkProperties and NetworkCapabilities, they are updated by the + // callbacks. mNetwork doesn't change except in onResume. + mNetworkInfo = mConnectivityManager.getNetworkInfo(mNetwork); + mWifiInfo = mWifiManager.getConnectionInfo(); + if (mNetwork == null || mNetworkInfo == null || mWifiInfo == null) { + exitActivity(); + return false; + } + + changed |= mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); + // If feature for saved network not enabled, always return true. + return mWifiTracker == null || changed; + } + + return changed; + } + + private void updateAccessPointFromScannedList() { + mIsOutOfRange = true; + + if (mAccessPoint.getConfig() == null) { + return; + } + + for (AccessPoint ap : mWifiTracker.getAccessPoints()) { + if (ap.getConfig() != null + && mAccessPoint.matches(ap.getConfig())) { + mAccessPoint = ap; + mIsOutOfRange = false; + return; + } + } } private void exitActivity() { @@ -452,14 +494,16 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mFragment.getActivity().finish(); } - private void refreshNetworkState() { - mAccessPoint.update(mWifiConfig, mWifiInfo, mNetworkInfo); - refreshEntityHeader(); - } - private void refreshRssiViews() { int signalLevel = mAccessPoint.getLevel(); + // Disappears signal view if not in range. e.g. for saved networks. + if (mIsOutOfRange) { + mSignalStrengthPref.setVisible(false); + mRssiSignalLevel = -1; + return; + } + if (mRssiSignalLevel == signalLevel) { return; } @@ -477,6 +521,84 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController mSignalStrengthPref.setIcon(wifiIconDark); mSignalStrengthPref.setSummary(mSignalStr[mRssiSignalLevel]); + mSignalStrengthPref.setVisible(true); + } + + private void refreshFrequency() { + if (mWifiInfo == null) { + mFrequencyPref.setVisible(false); + return; + } + + final int frequency = mWifiInfo.getFrequency(); + String band = null; + if (frequency >= AccessPoint.LOWER_FREQ_24GHZ + && frequency < AccessPoint.HIGHER_FREQ_24GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_24ghz); + } else if (frequency >= AccessPoint.LOWER_FREQ_5GHZ + && frequency < AccessPoint.HIGHER_FREQ_5GHZ) { + band = mContext.getResources().getString(R.string.wifi_band_5ghz); + } else { + Log.e(TAG, "Unexpected frequency " + frequency); + } + mFrequencyPref.setSummary(band); + mFrequencyPref.setVisible(true); + } + + private void refreshTxSpeed() { + if (mWifiInfo == null) { + mTxLinkSpeedPref.setVisible(false); + return; + } + + int txLinkSpeedMbps = mWifiInfo.getTxLinkSpeedMbps(); + mTxLinkSpeedPref.setVisible(txLinkSpeedMbps >= 0); + mTxLinkSpeedPref.setSummary(mContext.getString( + R.string.tx_link_speed, mWifiInfo.getTxLinkSpeedMbps())); + } + + private void refreshRxSpeed() { + if (mWifiInfo == null) { + mRxLinkSpeedPref.setVisible(false); + return; + } + + int rxLinkSpeedMbps = mWifiInfo.getRxLinkSpeedMbps(); + mRxLinkSpeedPref.setVisible(rxLinkSpeedMbps >= 0); + mRxLinkSpeedPref.setSummary(mContext.getString( + R.string.rx_link_speed, mWifiInfo.getRxLinkSpeedMbps())); + } + + private void refreshMacAddress() { + String macAddress = getMacAddress(); + if (macAddress == null) { + mMacAddressPref.setVisible(false); + return; + } + + mMacAddressPref.setVisible(true); + mMacAddressPref.setSummary(macAddress); + } + + private String getMacAddress() { + if (mWifiInfo != null) { + // get MAC address from connected network information + return mWifiInfo.getMacAddress(); + } + + // return randomized MAC address + if (mWifiConfig.macRandomizationSetting == WifiConfiguration.RANDOMIZATION_PERSISTENT) { + return mWifiConfig.getRandomizedMacAddress().toString(); + } + + // return device MAC address + final String[] macAddresses = mWifiManager.getFactoryMacAddresses(); + if (macAddresses != null && macAddresses.length > 0) { + return macAddresses[0]; + } + + Log.e(TAG, "Can't get device MAC address!"); + return null; } private void updatePreference(Preference pref, String detailText) { @@ -488,13 +610,17 @@ public class WifiDetailPreferenceController extends AbstractPreferenceController } } - private void updateIpLayerInfo() { + private void refreshButtons() { + mButtonsPref.setButton1Visible(canForgetNetwork()); mButtonsPref.setButton2Visible(canSignIntoNetwork()); mButtonsPref.setButton3Visible(canShareNetwork()); mButtonsPref.setVisible( canSignIntoNetwork() || canForgetNetwork() || canShareNetwork()); + } - if (mNetwork == null || mLinkProperties == null) { + private void updateIpLayerInfo() { + // Hide IP layer info if not a connected network. + if (!mAccessPoint.isActive() || mNetwork == null || mLinkProperties == null) { mIpAddressPref.setVisible(false); mSubnetPref.setVisible(false); mGatewayPref.setVisible(false); diff --git a/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java deleted file mode 100644 index 3407890aaf3..00000000000 --- a/src/com/android/settings/wifi/details/WifiDetailSavedNetworkPreferenceController.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.wifi.details; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.wifi.WifiManager; -import android.os.Handler; - -import androidx.fragment.app.Fragment; - -import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import com.android.settingslib.core.lifecycle.Lifecycle; -import com.android.settingslib.wifi.AccessPoint; - -public class WifiDetailSavedNetworkPreferenceController extends WifiDetailPreferenceController { - - WifiDetailSavedNetworkPreferenceController(AccessPoint accessPoint, - ConnectivityManager connectivityManager, Context context, - Fragment fragment, Handler handler, - Lifecycle lifecycle, - WifiManager wifiManager, - MetricsFeatureProvider metricsFeatureProvider, - IconInjector injector) { - super(accessPoint, connectivityManager, context, fragment, handler, lifecycle, wifiManager, - metricsFeatureProvider, injector); - } - - public static WifiDetailSavedNetworkPreferenceController newInstance( - AccessPoint accessPoint, - ConnectivityManager connectivityManager, - Context context, - Fragment fragment, - Handler handler, - Lifecycle lifecycle, - WifiManager wifiManager, - MetricsFeatureProvider metricsFeatureProvider) { - return new WifiDetailSavedNetworkPreferenceController( - accessPoint, connectivityManager, context, fragment, handler, lifecycle, - wifiManager, metricsFeatureProvider, new IconInjector(context)); - } - - @Override - public void onPause() { - // Do nothing - } - - @Override - public void onResume() { - updateSavedNetworkInfo(); - } -} diff --git a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java index 7edd227a559..66587edb54d 100644 --- a/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java +++ b/src/com/android/settings/wifi/details/WifiNetworkDetailsFragment.java @@ -51,9 +51,6 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { private static final String TAG = "WifiNetworkDetailsFrg"; - // Extra for if current fragment shows saved network status or not. - public static final String EXTRA_IS_SAVED_NETWORK = "SavedNetwork"; - private AccessPoint mAccessPoint; private WifiDetailPreferenceController mWifiDetailPreferenceController; @@ -126,30 +123,15 @@ public class WifiNetworkDetailsFragment extends DashboardFragment { final List controllers = new ArrayList<>(); final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); - final boolean isDisplaySavedNetworkDetails = - getArguments().getBoolean(EXTRA_IS_SAVED_NETWORK, false /* defaultValue */); - if (isDisplaySavedNetworkDetails) { - mWifiDetailPreferenceController = - WifiDetailSavedNetworkPreferenceController.newInstance( - mAccessPoint, - cm, - context, - this, - new Handler(Looper.getMainLooper()), // UI thread. - getSettingsLifecycle(), - context.getSystemService(WifiManager.class), - mMetricsFeatureProvider); - } else { - mWifiDetailPreferenceController = WifiDetailPreferenceController.newInstance( - mAccessPoint, - cm, - context, - this, - new Handler(Looper.getMainLooper()), // UI thread. - getSettingsLifecycle(), - context.getSystemService(WifiManager.class), - mMetricsFeatureProvider); - } + mWifiDetailPreferenceController = WifiDetailPreferenceController.newInstance( + mAccessPoint, + cm, + context, + this, + new Handler(Looper.getMainLooper()), // UI thread. + getSettingsLifecycle(), + context.getSystemService(WifiManager.class), + mMetricsFeatureProvider); controllers.add(mWifiDetailPreferenceController); controllers.add(new WifiMeteredPreferenceController(context, mAccessPoint.getConfig())); diff --git a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java index ea858f32361..3f600e6772d 100644 --- a/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java +++ b/src/com/android/settings/wifi/savedaccesspoints/SavedAccessPointsWifiSettings.java @@ -108,7 +108,6 @@ public class SavedAccessPointsWifiSettings extends DashboardFragment } final Bundle savedState = new Bundle(); mSelectedAccessPoint.saveWifiState(savedState); - savedState.putBoolean(WifiNetworkDetailsFragment.EXTRA_IS_SAVED_NETWORK, true); new SubSettingLauncher(getContext()) .setTitleText(mSelectedAccessPoint.getTitle()) From 8308274058b2d1cd5f639d5890dca3a3cfca6fe6 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Thu, 14 Mar 2019 13:50:36 -0700 Subject: [PATCH 03/13] Add circle background outline to unthethered icon Fixes: 124247230 Test: manual Change-Id: I70f8158685134e2115f2a7baded983ebac6c8f1b --- res/color/circle_outline_color.xml | 20 ++++++++++++++++++++ res/drawable/circle_outline.xml | 22 ++++++++++++++++++++++ res/layout/advanced_bt_entity_sub.xml | 6 ++++-- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 res/color/circle_outline_color.xml create mode 100644 res/drawable/circle_outline.xml diff --git a/res/color/circle_outline_color.xml b/res/color/circle_outline_color.xml new file mode 100644 index 00000000000..eb4e83c77e6 --- /dev/null +++ b/res/color/circle_outline_color.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/circle_outline.xml b/res/drawable/circle_outline.xml new file mode 100644 index 00000000000..1b2631d522d --- /dev/null +++ b/res/drawable/circle_outline.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/res/layout/advanced_bt_entity_sub.xml b/res/layout/advanced_bt_entity_sub.xml index 07ea8146fad..0f305830bdf 100644 --- a/res/layout/advanced_bt_entity_sub.xml +++ b/res/layout/advanced_bt_entity_sub.xml @@ -26,9 +26,11 @@ android:id="@+id/header_icon" android:layout_width="72dp" android:layout_height="72dp" - android:scaleType="fitCenter" android:layout_gravity="center_horizontal" - android:antialias="true"/> + android:antialias="true" + android:background="@drawable/circle_outline" + android:padding="8dp" + android:scaleType="fitCenter"/> Date: Tue, 12 Mar 2019 17:17:23 +0800 Subject: [PATCH 04/13] Sound + Output Switcher on Volume Slice - Show "play media to" item when Previously Connected device is available - Click "Play media to" to launch output slice - Update test case Bug: 127729340 Test: make -j50 RunSettingsRoboTests Change-Id: Ic00b309f87bc16f540b22b5a43fecb86f76caeb2 --- .../media/MediaOutputIndicatorSlice.java | 104 +++++++++++ .../media/MediaOutputIndicatorWorker.java | 161 ++++++++++++++++++ .../android/settings/panel/VolumePanel.java | 2 + .../settings/slices/CustomSliceRegistry.java | 13 +- .../media/MediaOutputIndicatorSliceTest.java | 109 ++++++++++++ .../media/MediaOutputIndicatorWorkerTest.java | 161 ++++++++++++++++++ .../settings/panel/VolumePanelTest.java | 1 + 7 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/media/MediaOutputIndicatorSlice.java create mode 100644 src/com/android/settings/media/MediaOutputIndicatorWorker.java create mode 100644 tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java create mode 100644 tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java diff --git a/src/com/android/settings/media/MediaOutputIndicatorSlice.java b/src/com/android/settings/media/MediaOutputIndicatorSlice.java new file mode 100644 index 00000000000..eb0c81f1c2c --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorSlice.java @@ -0,0 +1,104 @@ +/* + * 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import android.annotation.ColorInt; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.VisibleForTesting; +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.slices.CustomSliceable; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.media.MediaOutputSliceConstants; + +public class MediaOutputIndicatorSlice implements CustomSliceable { + + private Context mContext; + @VisibleForTesting + MediaOutputIndicatorWorker mWorker; + + public MediaOutputIndicatorSlice(Context context) { + mContext = context; + } + + @Override + public Slice getSlice() { + if (!getWorker().isVisible()) { + return null; + } + final IconCompat icon = IconCompat.createWithResource(mContext, + com.android.internal.R.drawable.ic_settings_bluetooth); + final CharSequence title = mContext.getText(R.string.media_output_title); + final PendingIntent primaryActionIntent = PendingIntent.getActivity(mContext, + 0 /* requestCode */, getMediaOutputSliceIntent(), 0 /* flags */); + final SliceAction primarySliceAction = SliceAction.createDeeplink( + primaryActionIntent, icon, ListBuilder.ICON_IMAGE, title); + @ColorInt final int color = Utils.getColorAccentDefaultColor(mContext); + + final ListBuilder listBuilder = new ListBuilder(mContext, + MEDIA_OUTPUT_INDICATOR_SLICE_URI, + ListBuilder.INFINITY) + .setAccentColor(color) + .addRow(new ListBuilder.RowBuilder() + .setTitle(title) + .setSubtitle(getWorker().findActiveDeviceName()) + .setPrimaryAction(primarySliceAction)); + return listBuilder.build(); + } + + private MediaOutputIndicatorWorker getWorker() { + if (mWorker == null) { + mWorker = (MediaOutputIndicatorWorker) SliceBackgroundWorker.getInstance(getUri()); + } + return mWorker; + } + + private Intent getMediaOutputSliceIntent() { + final Intent intent = new Intent() + .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } + + @Override + public Uri getUri() { + return MEDIA_OUTPUT_INDICATOR_SLICE_URI; + } + + @Override + public Intent getIntent() { + // This Slice reflects active media device information and launch MediaOutputSlice. It does + // not contain its owned Slice data + return null; + } + + @Override + public Class getBackgroundWorkerClass() { + return MediaOutputIndicatorWorker.class; + } +} diff --git a/src/com/android/settings/media/MediaOutputIndicatorWorker.java b/src/com/android/settings/media/MediaOutputIndicatorWorker.java new file mode 100644 index 00000000000..adee0557be9 --- /dev/null +++ b/src/com/android/settings/media/MediaOutputIndicatorWorker.java @@ -0,0 +1,161 @@ +/* + * 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.media; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.android.internal.util.CollectionUtils; +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.slices.SliceBackgroundWorker; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Listener for background change from {@code BluetoothCallback} to update media output indicator. + */ +public class MediaOutputIndicatorWorker extends SliceBackgroundWorker implements BluetoothCallback { + + private static final String TAG = "MediaOutputIndicatorWorker"; + + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + public MediaOutputIndicatorWorker(Context context, Uri uri) { + super(context, uri); + } + + @Override + protected void onSlicePinned() { + LocalBluetoothManager mLocalBluetoothManager = Utils.getLocalBtManager(getContext()); + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mProfileManager = mLocalBluetoothManager.getProfileManager(); + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + + @Override + protected void onSliceUnpinned() { + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + + @Override + public void close() throws IOException { + mLocalBluetoothManager = null; + mProfileManager = null; + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + // To handle the case that Bluetooth on and no connected devices + notifySliceChange(); + } + + @Override + public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) { + if (bluetoothProfile == BluetoothProfile.A2DP) { + notifySliceChange(); + } + } + + /** + * To decide Slice's visibility. + * + * @return true if device is connected or previously connected, false for other cases. + */ + public boolean isVisible() { + return !CollectionUtils.isEmpty(getConnectableA2dpDevices()) + || !CollectionUtils.isEmpty(getConnectableHearingAidDevices()); + } + + private List getConnectableA2dpDevices() { + // get A2dp devices on all states + // (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile == null) { + return new ArrayList<>(); + } + return a2dpProfile.getConnectableDevices(); + } + + private List getConnectableHearingAidDevices() { + // get hearing aid profile devices on all states + // (STATE_DISCONNECTED, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING) + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + if (hapProfile == null) { + return new ArrayList<>(); + } + + return hapProfile.getConnectableDevices(); + } + + /** + * Get active devices name. + * + * @return active Bluetooth device alias, or default summary if no active device. + */ + public CharSequence findActiveDeviceName() { + // Return Hearing Aid device name if it is active + BluetoothDevice activeDevice = findActiveHearingAidDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + // Return A2DP device name if it is active + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile != null) { + activeDevice = a2dpProfile.getActiveDevice(); + if (activeDevice != null) { + return activeDevice.getAliasName(); + } + } + // No active device, return default summary + return getContext().getText(R.string.media_output_default_summary); + } + + private BluetoothDevice findActiveHearingAidDevice() { + final HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile(); + if (hearingAidProfile == null) { + return null; + } + + final List activeDevices = hearingAidProfile.getActiveDevices(); + for (BluetoothDevice btDevice : activeDevices) { + if (btDevice != null) { + return btDevice; + } + } + return null; + } +} diff --git a/src/com/android/settings/panel/VolumePanel.java b/src/com/android/settings/panel/VolumePanel.java index 62eca53898b..4ea7fe7561b 100644 --- a/src/com/android/settings/panel/VolumePanel.java +++ b/src/com/android/settings/panel/VolumePanel.java @@ -16,6 +16,7 @@ package com.android.settings.panel; +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_ALARM_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_CALL_URI; import static com.android.settings.slices.CustomSliceRegistry.VOLUME_MEDIA_URI; @@ -55,6 +56,7 @@ public class VolumePanel implements PanelContent { final List uris = new ArrayList<>(); uris.add(VOLUME_REMOTE_MEDIA_URI); uris.add(VOLUME_MEDIA_URI); + uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI); uris.add(VOLUME_CALL_URI); uris.add(VOLUME_RINGER_URI); uris.add(VOLUME_ALARM_URI); diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java index 12e7b48ffb2..3c9b17b35b1 100644 --- a/src/com/android/settings/slices/CustomSliceRegistry.java +++ b/src/com/android/settings/slices/CustomSliceRegistry.java @@ -39,6 +39,7 @@ import com.android.settings.homepage.contextualcards.slices.BluetoothDevicesSlic 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.MediaOutputIndicatorSlice; import com.android.settings.media.MediaOutputSlice; import com.android.settings.network.telephony.MobileDataSlice; import com.android.settings.wifi.calling.WifiCallingSliceHelper; @@ -299,6 +300,16 @@ public class CustomSliceRegistry { .appendPath(MediaOutputSliceConstants.KEY_MEDIA_OUTPUT) .build(); + /** + * Backing Uri for the Media output indicator Slice. + */ + public static Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT) + .appendPath("media_output_indicator") + .build(); + @VisibleForTesting static final Map> sUriToSlice; @@ -319,6 +330,7 @@ public class CustomSliceRegistry { sUriToSlice.put(STORAGE_SLICE_URI, StorageSlice.class); sUriToSlice.put(WIFI_SLICE_URI, WifiSlice.class); sUriToSlice.put(MEDIA_OUTPUT_SLICE_URI, MediaOutputSlice.class); + sUriToSlice.put(MEDIA_OUTPUT_INDICATOR_SLICE_URI, MediaOutputIndicatorSlice.class); } public static Class getSliceClassByUri(Uri uri) { @@ -344,5 +356,4 @@ public class CustomSliceRegistry { public static boolean isValidAction(String action) { return isValidUri(Uri.parse(action)); } - } diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java new file mode 100644 index 00000000000..ab3f4de26f9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorSliceTest.java @@ -0,0 +1,109 @@ +/* + * 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.media; + +import static com.android.settings.slices.CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; + +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.SliceMetadata; +import androidx.slice.SliceProvider; +import androidx.slice.core.SliceAction; +import androidx.slice.widget.SliceLiveData; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.media.LocalMediaManager; +import com.android.settingslib.media.MediaDevice; +import com.android.settingslib.media.MediaOutputSliceConstants; + +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 org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) +public class MediaOutputIndicatorSliceTest { + + private static final String TEST_DEVICE_NAME = "test_device_name"; + private static final int TEST_DEVICE_1_ICON = + com.android.internal.R.drawable.ic_bt_headphones_a2dp; + + @Mock + private LocalMediaManager mLocalMediaManager; + + private final List mDevices = new ArrayList<>(); + + private Context mContext; + private MediaOutputIndicatorSlice mMediaOutputIndicatorSlice; + private MediaOutputIndicatorWorker mMediaOutputIndicatorWorker; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + + // Set-up specs for SliceMetadata. + SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS); + + mMediaOutputIndicatorSlice = new MediaOutputIndicatorSlice(mContext); + mMediaOutputIndicatorWorker = spy(new MediaOutputIndicatorWorker( + mContext, MEDIA_OUTPUT_INDICATOR_SLICE_URI)); + mMediaOutputIndicatorSlice.mWorker = mMediaOutputIndicatorWorker; + } + + @Test + public void getSlice_invisible_returnNull() { + when(mMediaOutputIndicatorWorker.isVisible()).thenReturn(false); + + assertThat(mMediaOutputIndicatorSlice.getSlice()).isNull(); + } + + @Test + public void getSlice_withActiveDevice_checkContent() { + when(mMediaOutputIndicatorWorker.isVisible()).thenReturn(true); + when(mMediaOutputIndicatorWorker.findActiveDeviceName()).thenReturn(TEST_DEVICE_NAME); + final Slice mediaSlice = mMediaOutputIndicatorSlice.getSlice(); + final SliceMetadata metadata = SliceMetadata.from(mContext, mediaSlice); + // Verify slice title and subtitle + assertThat(metadata.getTitle()).isEqualTo(mContext.getText(R.string.media_output_title)); + assertThat(metadata.getSubtitle()).isEqualTo(TEST_DEVICE_NAME); + } +} diff --git a/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java new file mode 100644 index 00000000000..4a5662e53de --- /dev/null +++ b/tests/robotests/src/com/android/settings/media/MediaOutputIndicatorWorkerTest.java @@ -0,0 +1,161 @@ +/* + * 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.media; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.net.Uri; + +import com.android.settings.R; +import com.android.settings.bluetooth.Utils; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +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 org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowBluetoothDevice; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothUtils.class, + ShadowBluetoothDevice.class}) +public class MediaOutputIndicatorWorkerTest { + + private static final String TEST_A2DP_DEVICE_NAME = "Test_A2DP_BT_Device_NAME"; + private static final String TEST_HAP_DEVICE_NAME = "Test_HAP_BT_Device_NAME"; + private static final String TEST_A2DP_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"; + private static final String TEST_HAP_DEVICE_ADDRESS = "00:B2:B2:B2:B2:B2"; + private static final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + + @Mock + private A2dpProfile mA2dpProfile; + @Mock + private HearingAidProfile mHearingAidProfile; + @Mock + private LocalBluetoothManager mLocalManager; + @Mock + private BluetoothEventManager mBluetoothEventManager; + @Mock + private LocalBluetoothProfileManager mLocalBluetoothProfileManager; + + private BluetoothAdapter mBluetoothAdapter; + private BluetoothDevice mA2dpDevice; + private BluetoothDevice mHapDevice; + private BluetoothManager mBluetoothManager; + private Context mContext; + private List mDevicesList; + private LocalBluetoothManager mLocalBluetoothManager; + private MediaOutputIndicatorWorker mMediaDeviceUpdateWorker; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBluetoothProfileManager); + when(mLocalBluetoothProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mLocalBluetoothProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + mBluetoothManager = new BluetoothManager(mContext); + mBluetoothAdapter = mBluetoothManager.getAdapter(); + + // Setup A2dp device + mA2dpDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_A2DP_DEVICE_ADDRESS)); + when(mA2dpDevice.getName()).thenReturn(TEST_A2DP_DEVICE_NAME); + when(mA2dpDevice.isConnected()).thenReturn(true); + // Setup HearingAid device + mHapDevice = spy(mBluetoothAdapter.getRemoteDevice(TEST_HAP_DEVICE_ADDRESS)); + when(mHapDevice.getName()).thenReturn(TEST_HAP_DEVICE_NAME); + when(mHapDevice.isConnected()).thenReturn(true); + + mMediaDeviceUpdateWorker = new MediaOutputIndicatorWorker(mContext, URI); + mDevicesList = new ArrayList<>(); + } + + @Test + public void isVisible_noConnectableDevice_returnFalse() { + mDevicesList.clear(); + when(mA2dpProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isFalse(); + } + + @Test + public void isVisible_withConnectableA2dpDevice_returnTrue() { + mDevicesList.clear(); + mDevicesList.add(mA2dpDevice); + when(mHearingAidProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isTrue(); + } + + @Test + public void isVisible_withConnectableHADevice_returnTrue() { + mDevicesList.clear(); + mDevicesList.add(mHapDevice); + when(mA2dpProfile.getConnectableDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.isVisible()).isTrue(); + } + + @Test + public void findActiveDeviceName_A2dpDeviceActive_verifyName() { + when(mA2dpProfile.getActiveDevice()).thenReturn(mA2dpDevice); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mA2dpDevice.getAliasName()); + } + + @Test + public void findActiveDeviceName_HADeviceActive_verifyName() { + mDevicesList.add(mHapDevice); + when(mHearingAidProfile.getActiveDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mHapDevice.getAliasName()); + } + + @Test + public void findActiveDeviceName_noActiveDevice_verifyDefaultName() { + when(mA2dpProfile.getActiveDevice()).thenReturn(null); + mDevicesList.clear(); + when(mHearingAidProfile.getActiveDevices()).thenReturn(mDevicesList); + + assertThat(mMediaDeviceUpdateWorker.findActiveDeviceName()) + .isEqualTo(mContext.getText(R.string.media_output_default_summary)); + } +} diff --git a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java index 4665dc9b4fd..11de7b31b60 100644 --- a/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java +++ b/tests/robotests/src/com/android/settings/panel/VolumePanelTest.java @@ -48,6 +48,7 @@ public class VolumePanelTest { CustomSliceRegistry.VOLUME_REMOTE_MEDIA_URI, CustomSliceRegistry.VOLUME_CALL_URI, CustomSliceRegistry.VOLUME_MEDIA_URI, + CustomSliceRegistry.MEDIA_OUTPUT_INDICATOR_SLICE_URI, CustomSliceRegistry.VOLUME_RINGER_URI, CustomSliceRegistry.VOLUME_ALARM_URI); } From 66ac958993d05b458371f97100f936611380c916 Mon Sep 17 00:00:00 2001 From: Annie Meng Date: Mon, 18 Mar 2019 17:18:39 +0000 Subject: [PATCH 05/13] Update OWNERS for backup settings Test: build/make/tools/checkowners.py -c -v OWNERS Change-Id: Ib98d9c9ef2b538b7d620e1361d0e48d263635449 --- src/com/android/settings/backup/OWNERS | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/backup/OWNERS b/src/com/android/settings/backup/OWNERS index c026a350e75..a7b55fd134b 100644 --- a/src/com/android/settings/backup/OWNERS +++ b/src/com/android/settings/backup/OWNERS @@ -1,6 +1,13 @@ -# Default reviewers for this and subdirectories. +# Use this reviewer by default. +br-framework-team+reviews@google.com + +# People who can approve changes for submission. +anniemeng@google.com +nathch@google.com +rthakohov@google.com + +# Others (in case above are not available). bryanmawhinney@google.com -cprins@google.com jorlow@google.com philippov@google.com stefanot@google.com \ No newline at end of file From 5bb2a87b7f6bd6d87e1d01720005e1f90fbf40db Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Fri, 15 Mar 2019 16:41:24 -0700 Subject: [PATCH 06/13] Create a page to manage dnd permission for individual app - Change the original ZenAccessPage to - Remove the inline switch - Make the preference click target go into the new detail page - Some formatting/style change. - Create a new detail page for zen access. - Exit if app didn't declare this permission - Preset the switch toggle to their current permission grant state - Move the warning dialog logic from ZenAccessSettings to here. - Move some common functionality into ZenAccessController, a static helper class Bug: 128547723 Test: robotest Change-Id: I1ebb32396869d07ff4283b300bd716506298c9b5 --- res/values/strings.xml | 3 + res/xml/zen_access_permission_details.xml | 27 ++ .../FriendlyWarningDialogFragment.java | 76 ++++++ .../zenaccess/ScaryWarningDialogFragment.java | 73 +++++ .../zenaccess/ZenAccessController.java | 79 +++++- .../zenaccess/ZenAccessDetails.java | 100 +++++++ .../ZenAccessSettingObserverMixin.java | 77 ++++++ .../notification/ZenAccessSettings.java | 256 +++--------------- .../zenaccess/ZenAccessControllerTest.java | 43 +++ .../ZenAccessSettingObserverMixinTest.java | 109 ++++++++ .../notification/ZenAccessSettingsTest.java | 63 ----- .../shadow/ShadowNotificationManager.java | 13 + 12 files changed, 635 insertions(+), 284 deletions(-) create mode 100644 res/xml/zen_access_permission_details.xml create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java create mode 100644 tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java delete mode 100644 tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index e4d0d115f0d..faef8a0528c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7939,6 +7939,9 @@ Do Not Disturb access + + Allow Do Not Disturb + No installed apps have requested Do Not Disturb access diff --git a/res/xml/zen_access_permission_details.xml b/res/xml/zen_access_permission_details.xml new file mode 100644 index 00000000000..afa8d80a17c --- /dev/null +++ b/res/xml/zen_access_permission_details.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java new file mode 100644 index 00000000000..fc85f7dffd0 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java @@ -0,0 +1,76 @@ +/* + * 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.applications.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Warning dialog when revoking zen access warning that zen rule instances will be deleted. + */ +public class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; + } + + public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString( + R.string.zen_access_revoke_warning_dialog_title, label); + final String summary = getResources() + .getString(R.string.zen_access_revoke_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.okay, + (dialog, id) -> { + ZenAccessController.deleteRules(getContext(), pkg); + ZenAccessController.setAccess(getContext(), pkg, false); + }) + .setNegativeButton(R.string.cancel, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java new file mode 100644 index 00000000000..69318f8d6af --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java @@ -0,0 +1,73 @@ +/* + * 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.applications.specialaccess.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.notification.ZenAccessSettings; + +/** + * Warning dialog when allowing zen access warning about the privileges being granted. + */ +public class ScaryWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; + } + + public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString(R.string.zen_access_warning_dialog_title, + label); + final String summary = getResources() + .getString(R.string.zen_access_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.allow, + (dialog, id) -> ZenAccessController.setAccess(getContext(), pkg, true)) + .setNegativeButton(R.string.deny, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java index 88d444d485c..946599b4aa8 100644 --- a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java @@ -17,12 +17,29 @@ package com.android.settings.applications.specialaccess.zenaccess; import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.ParceledListSlice; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; + +import java.util.List; +import java.util.Set; public class ZenAccessController extends BasePreferenceController { + private static final String TAG = "ZenAccessController"; + private final ActivityManager mActivityManager; public ZenAccessController(Context context, String preferenceKey) { @@ -32,8 +49,68 @@ public class ZenAccessController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return !mActivityManager.isLowRamDevice() + return isSupported(mActivityManager) ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } + + public static boolean isSupported(ActivityManager activityManager) { + return !activityManager.isLowRamDevice(); + } + + public static Set getPackagesRequestingNotificationPolicyAccess() { + final ArraySet requestingPackages = new ArraySet<>(); + try { + final String[] PERM = { + android.Manifest.permission.ACCESS_NOTIFICATION_POLICY + }; + final ParceledListSlice list = AppGlobals.getPackageManager() + .getPackagesHoldingPermissions(PERM, 0 /*flags*/, + ActivityManager.getCurrentUser()); + final List pkgs = list.getList(); + if (pkgs != null) { + for (PackageInfo info : pkgs) { + requestingPackages.add(info.packageName); + } + } + } catch (RemoteException e) { + Log.e(TAG, "Cannot reach packagemanager", e); + } + return requestingPackages; + } + + public static Set getAutoApprovedPackages(Context context) { + final Set autoApproved = new ArraySet<>(); + autoApproved.addAll(context.getSystemService(NotificationManager.class) + .getEnabledNotificationListenerPackages()); + return autoApproved; + } + + public static boolean hasAccess(Context context, String pkg) { + return context.getSystemService( + NotificationManager.class).isNotificationPolicyAccessGrantedForPackage(pkg); + } + + public static void setAccess(final Context context, final String pkg, final boolean access) { + logSpecialPermissionChange(access, pkg, context); + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.setNotificationPolicyAccessGranted(pkg, access); + }); + } + + public static void deleteRules(final Context context, final String pkg) { + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.removeAutomaticZenRules(pkg); + }); + } + + @VisibleForTesting + static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; + FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, + logCategory, packageName); + } } diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java new file mode 100644 index 00000000000..a18e7d63cad --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java @@ -0,0 +1,100 @@ +/* + * 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.applications.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoWithHeader; + +import java.util.Set; + +public class ZenAccessDetails extends AppInfoWithHeader implements + ZenAccessSettingObserverMixin.Listener { + + private static final String SWITCH_PREF_KEY = "zen_access_switch"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_ACCESS_DETAIL; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.zen_access_permission_details); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); + } + + @Override + protected boolean refreshUi() { + final Context context = getContext(); + if (!ZenAccessController.isSupported(context.getSystemService(ActivityManager.class))) { + return false; + } + // If this app didn't declare this permission in their manifest, don't bother showing UI. + final Set needAccessApps = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); + if (!needAccessApps.contains(mPackageName)) { + return false; + } + updatePreference(context, findPreference(SWITCH_PREF_KEY)); + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + public void updatePreference(Context context, SwitchPreference preference) { + final CharSequence label = mPackageInfo.applicationInfo.loadLabel(mPm); + final Set autoApproved = ZenAccessController.getAutoApprovedPackages(context); + if (autoApproved.contains(mPackageName)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. + preference.setEnabled(false); + preference.setSummary(getString(R.string.zen_access_disabled_package_warning)); + return; + } + preference.setChecked(ZenAccessController.hasAccess(context, mPackageName)); + preference.setOnPreferenceChangeListener((p, newValue) -> { + final boolean access = (Boolean) newValue; + if (access) { + new ScaryWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } else { + new FriendlyWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } + return false; + }); + } + + @Override + public void onZenAccessPolicyChanged() { + refreshUi(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java new file mode 100644 index 00000000000..30507efffa9 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java @@ -0,0 +1,77 @@ +/* + * 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.applications.specialaccess.zenaccess; + +import android.app.ActivityManager; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class ZenAccessSettingObserverMixin extends ContentObserver implements LifecycleObserver, + OnStart, OnStop { + + public interface Listener { + void onZenAccessPolicyChanged(); + } + + private final Context mContext; + private final Listener mListener; + + public ZenAccessSettingObserverMixin(Context context, Listener listener) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mListener = listener; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mListener != null) { + mListener.onZenAccessPolicyChanged(); + } + } + + @Override + public void onStart() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), + false /* notifyForDescendants */, + this /* observer */); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), + false /* notifyForDescendants */, + this /* observer */); + } + + @Override + public void onStop() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().unregisterContentObserver(this /* observer */); + } +} diff --git a/src/com/android/settings/notification/ZenAccessSettings.java b/src/com/android/settings/notification/ZenAccessSettings.java index d057c755754..fca82552244 100644 --- a/src/com/android/settings/notification/ZenAccessSettings.java +++ b/src/com/android/settings/notification/ZenAccessSettings.java @@ -18,56 +18,40 @@ package com.android.settings.notification; import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.AppGlobals; -import android.app.Dialog; import android.app.NotificationManager; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; import android.provider.SearchIndexableResource; -import android.provider.Settings.Secure; -import android.text.TextUtils; import android.util.ArraySet; -import android.util.Log; import android.view.View; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.overlay.FeatureFactory; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessController; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessSettingObserverMixin; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.widget.AppSwitchPreference; import com.android.settings.widget.EmptyTextSettings; import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; @SearchIndexable -public class ZenAccessSettings extends EmptyTextSettings { +public class ZenAccessSettings extends EmptyTextSettings implements + ZenAccessSettingObserverMixin.Listener { private final String TAG = "ZenAccessSettings"; - private final SettingObserver mObserver = new SettingObserver(); private Context mContext; private PackageManager mPkgMan; private NotificationManager mNoMan; @@ -84,6 +68,8 @@ public class ZenAccessSettings extends EmptyTextSettings { mContext = getActivity(); mPkgMan = mContext.getPackageManager(); mNoMan = mContext.getSystemService(NotificationManager.class); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); } @Override @@ -102,30 +88,22 @@ public class ZenAccessSettings extends EmptyTextSettings { super.onResume(); if (!ActivityManager.isLowRamDeviceStatic()) { reloadList(); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false, - mObserver); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false, - mObserver); } else { setEmptyText(R.string.disabled_low_ram_device); } } @Override - public void onPause() { - super.onPause(); - if (!ActivityManager.isLowRamDeviceStatic()) { - getContentResolver().unregisterContentObserver(mObserver); - } + public void onZenAccessPolicyChanged() { + reloadList(); } private void reloadList() { final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); final ArrayList apps = new ArrayList<>(); - final ArraySet requesting = getPackagesRequestingNotificationPolicyAccess(); + final Set requesting = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); if (!requesting.isEmpty()) { final List installed = mPkgMan.getInstalledApplications(0); if (installed != null) { @@ -143,204 +121,42 @@ public class ZenAccessSettings extends EmptyTextSettings { for (ApplicationInfo app : apps) { final String pkg = app.packageName; final CharSequence label = app.loadLabel(mPkgMan); - final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); + final AppPreference pref = new AppPreference(getPrefContext()); pref.setKey(pkg); - pref.setPersistent(false); pref.setIcon(app.loadIcon(mPkgMan)); pref.setTitle(label); - pref.setChecked(hasAccess(pkg)); if (autoApproved.contains(pkg)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. pref.setEnabled(false); pref.setSummary(getString(R.string.zen_access_disabled_package_warning)); + } else { + // Not auto approved, update summary according to notification backend. + pref.setSummary(getPreferenceSummary(pkg)); } - pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean access = (Boolean) newValue; - if (access) { - new ScaryWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } else { - new FriendlyWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } - return false; - } + pref.setOnPreferenceClickListener(preference -> { + AppInfoBase.startAppInfoFragment( + ZenAccessDetails.class /* fragment */, + R.string.manage_zen_access_title /* titleRes */, + pkg, + app.uid, + this /* source */, + -1 /* requestCode */, + getMetricsCategory() /* sourceMetricsCategory */); + return true; }); + screen.addPreference(pref); } } - private ArraySet getPackagesRequestingNotificationPolicyAccess() { - ArraySet requestingPackages = new ArraySet<>(); - try { - final String[] PERM = { - android.Manifest.permission.ACCESS_NOTIFICATION_POLICY - }; - final ParceledListSlice list = AppGlobals.getPackageManager() - .getPackagesHoldingPermissions(PERM, 0 /*flags*/, - ActivityManager.getCurrentUser()); - final List pkgs = list.getList(); - if (pkgs != null) { - for (PackageInfo info : pkgs) { - requestingPackages.add(info.packageName); - } - } - } catch(RemoteException e) { - Log.e(TAG, "Cannot reach packagemanager", e); - } - return requestingPackages; - } - - private boolean hasAccess(String pkg) { - return mNoMan.isNotificationPolicyAccessGrantedForPackage(pkg); - } - - private static void setAccess(final Context context, final String pkg, final boolean access) { - logSpecialPermissionChange(access, pkg, context); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.setNotificationPolicyAccessGranted(pkg, access); - } - }); - } - - @VisibleForTesting - static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { - int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW - : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; - FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - logCategory, packageName); - } - - - private static void deleteRules(final Context context, final String pkg) { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.removeAutomaticZenRules(pkg); - } - }); - } - - private final class SettingObserver extends ContentObserver { - public SettingObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - reloadList(); - } - } - /** - * Warning dialog when allowing zen access warning about the privileges being granted. + * @return the summary for the current state of whether the app associated with the given + * {@param packageName} is allowed to enter picture-in-picture. */ - public static class ScaryWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; - } - - public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString(R.string.zen_access_warning_dialog_title, - label); - final String summary = getResources() - .getString(R.string.zen_access_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.allow, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - setAccess(getContext(), pkg, true); - } - }) - .setNegativeButton(R.string.deny, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } - } - - /** - * Warning dialog when revoking zen access warning that zen rule instances will be deleted. - */ - public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; - } - - public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString( - R.string.zen_access_revoke_warning_dialog_title, label); - final String summary = getResources() - .getString(R.string.zen_access_revoke_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.okay, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - deleteRules(getContext(), pkg); - setAccess(getContext(), pkg, false); - } - }) - .setNegativeButton(R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } + private int getPreferenceSummary(String packageName) { + final boolean enabled = ZenAccessController.hasAccess(getContext(), packageName); + return enabled ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed; } public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java index bcb4bb3bf4a..6041e9dead1 100644 --- a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java @@ -18,26 +18,41 @@ package com.android.settings.applications.specialaccess.zenaccess; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.NotificationManager; import android.content.Context; +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowNotificationManager; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) public class ZenAccessControllerTest { + private static final String TEST_PKG = "com.test.package"; + + private FakeFeatureFactory mFeatureFactory; private Context mContext; private ZenAccessController mController; private ShadowActivityManager mActivityManager; + @Before public void setUp() { mContext = RuntimeEnvironment.application; + mFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new ZenAccessController(mContext, "key"); mActivityManager = Shadow.extract(mContext.getSystemService(Context.ACTIVITY_SERVICE)); } @@ -52,4 +67,32 @@ public class ZenAccessControllerTest { mActivityManager.setIsLowRamDevice(true); assertThat(mController.isAvailable()).isFalse(); } + + @Test + public void logSpecialPermissionChange() { + ZenAccessController.logSpecialPermissionChange(true, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), + eq("app")); + + ZenAccessController.logSpecialPermissionChange(false, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), + eq("app")); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_granted_yes() { + final ShadowNotificationManager snm = Shadow.extract(mContext.getSystemService( + NotificationManager.class)); + snm.setNotificationPolicyAccessGrantedForPackage(TEST_PKG); + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isTrue(); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_notGranted_no() { + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java new file mode 100644 index 00000000000..cba1a5199ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java @@ -0,0 +1,109 @@ +/* + * 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.applications.specialaccess.zenaccess; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.provider.Settings; + +import androidx.lifecycle.LifecycleOwner; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +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 org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowActivityManager; + +@RunWith(RobolectricTestRunner.class) +public class ZenAccessSettingObserverMixinTest { + + @Mock + private ZenAccessSettingObserverMixin.Listener mListener; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private ZenAccessSettingObserverMixin mMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + + mMixin = new ZenAccessSettingObserverMixin(mContext, mListener); + + mLifecycle.addObserver(mMixin); + } + + @Test + public void onStart_lowMemory_shouldNotRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(true); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } + + @Test + public void onStart_highMemory_shouldRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener).onZenAccessPolicyChanged(); + } + + @Test + public void onStop_shouldUnregisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + mLifecycle.handleLifecycleEvent(ON_STOP); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java b/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java deleted file mode 100644 index c2a6f4f892c..00000000000 --- a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2017 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.notification; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -import android.content.Context; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.testutils.FakeFeatureFactory; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ZenAccessSettingsTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; - - private FakeFeatureFactory mFeatureFactory; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mFeatureFactory = FakeFeatureFactory.setupForTest(); - } - - @Test - public void logSpecialPermissionChange() { - ZenAccessSettings.logSpecialPermissionChange(true, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), - eq("app")); - - ZenAccessSettings.logSpecialPermissionChange(false, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), - eq("app")); - } -} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java index 83257776289..78fb23f3133 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java @@ -19,15 +19,19 @@ package com.android.settings.testutils.shadow; import android.app.NotificationManager; import android.net.Uri; import android.service.notification.ZenModeConfig; +import android.util.ArraySet; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import java.util.Set; + @Implements(NotificationManager.class) public class ShadowNotificationManager { private int mZenMode; private ZenModeConfig mZenModeConfig; + private Set mNotificationPolicyGrantedPackages = new ArraySet<>(); @Implementation protected void setZenMode(int mode, Uri conditionId, String reason) { @@ -39,6 +43,11 @@ public class ShadowNotificationManager { return mZenMode; } + @Implementation + protected boolean isNotificationPolicyAccessGrantedForPackage(String pkg) { + return mNotificationPolicyGrantedPackages.contains(pkg); + } + @Implementation public ZenModeConfig getZenModeConfig() { return mZenModeConfig; @@ -47,4 +56,8 @@ public class ShadowNotificationManager { public void setZenModeConfig(ZenModeConfig config) { mZenModeConfig = config; } + + public void setNotificationPolicyAccessGrantedForPackage(String pkg) { + mNotificationPolicyGrantedPackages.add(pkg); + } } From 620d6aeccb20994da2fe43149aa3e005f4d2ba26 Mon Sep 17 00:00:00 2001 From: Yi-Ling Chuang Date: Wed, 6 Mar 2019 14:38:23 +0800 Subject: [PATCH 07/13] Log latency and timeout occurrence for contextual cards. Fixes: 124492762 Test: rebuild Change-Id: I552adf7e6d76e8576a790e83792d067ed5716dc2 --- .../contextualcards/ContextualCardLoader.java | 1 - .../ContextualCardManager.java | 16 ++++++++-- .../contextualcards/EligibleCardChecker.java | 31 ++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java index ff2ee91468e..1f5f7dd5694 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java @@ -187,7 +187,6 @@ public class ContextualCardLoader extends AsyncLoaderCompat // Collect future and eligible cards for (Future cardFuture : eligibleCards) { try { - //TODO(b/124492762): Log latency and timeout occurrence. final ContextualCard card = cardFuture.get(ELIGIBILITY_CHECKER_TIMEOUT_MS, TimeUnit.MILLISECONDS); if (card != null) { diff --git a/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java index 8f7e84acd2f..7df322d5211 100644 --- a/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java +++ b/src/com/android/settings/homepage/contextualcards/ContextualCardManager.java @@ -22,6 +22,7 @@ import static com.android.settings.intelligence.ContextualCardProto.ContextualCa import static java.util.stream.Collectors.groupingBy; +import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; import android.provider.Settings; @@ -38,6 +39,7 @@ import androidx.loader.content.Loader; import com.android.settings.homepage.contextualcards.slices.SliceContextualCardRenderer; import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnSaveInstanceState; @@ -195,8 +197,8 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo @Override public void onFinishCardLoading(List cards) { final long loadTime = System.currentTimeMillis() - mStartTime; - //TODO(b/123668403): remove the log here once we do the change with FutureTask Log.d(TAG, "Total loading time = " + loadTime); + final List cardsToKeep = getCardsToKeep(cards); //navigate back to the homepage, screen rotate or after card dismissal @@ -206,15 +208,25 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo return; } - //only log homepage display upon a fresh launch + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); final long timeoutLimit = getCardLoaderTimeout(mContext); if (loadTime <= timeoutLimit) { onContextualCardUpdated(cards.stream() .collect(groupingBy(ContextualCard::getCardType))); + } else { + // log timeout occurrence + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD_TIMEOUT, + SettingsEnums.SETTINGS_HOMEPAGE, + null /* key */, (int) loadTime /* value */); } + //only log homepage display upon a fresh launch final long totalTime = System.currentTimeMillis() - mStartTime; FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext) .logHomepageDisplay(totalTime); + metricsFeatureProvider.action(mContext, + SettingsEnums.ACTION_CONTEXTUAL_HOME_SHOW, (int) totalTime); mIsFirstLaunch = false; } diff --git a/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java index fe68d028533..811aaa2fa13 100644 --- a/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java +++ b/src/com/android/settings/homepage/contextualcards/EligibleCardChecker.java @@ -18,6 +18,7 @@ package com.android.settings.homepage.contextualcards; import static android.app.slice.Slice.HINT_ERROR; +import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; import android.net.Uri; @@ -27,6 +28,9 @@ import androidx.annotation.VisibleForTesting; import androidx.slice.Slice; import androidx.slice.SliceViewManager; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -46,7 +50,32 @@ public class EligibleCardChecker implements Callable { @Override public ContextualCard call() throws Exception { - return isCardEligibleToDisplay(mCard) ? mCard : null; + final long startTime = System.currentTimeMillis(); + final MetricsFeatureProvider metricsFeatureProvider = + FeatureFactory.getFactory(mContext).getMetricsFeatureProvider(); + ContextualCard result; + + if (isCardEligibleToDisplay(mCard)) { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 1 /* true */); + result = mCard; + } else { + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_ELIGIBILITY, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, 0 /* false */); + result = null; + } + // Log individual card loading time + metricsFeatureProvider.action(SettingsEnums.PAGE_UNKNOWN, + SettingsEnums.ACTION_CONTEXTUAL_CARD_LOAD, + SettingsEnums.SETTINGS_HOMEPAGE, + mCard.getTextSliceUri() /* key */, + (int) (System.currentTimeMillis() - startTime) /* value */); + + return result; } @VisibleForTesting From d3ae43015044a6b8bc084ea6671b30daabcd7acc Mon Sep 17 00:00:00 2001 From: Yanting Yang Date: Tue, 19 Mar 2019 19:54:04 +0800 Subject: [PATCH 08/13] Update strings of notification channel slice Bug:128641319 Test: manual Change-Id: Ia5f5ee770a28c0720526e60ffdb48fb2bd3de13f --- res/values/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index aa43f12f4b6..0283b5713b5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10767,7 +10767,7 @@ - Manage %1$s Notifications + Manage %1$s notifications No suggested application @@ -10777,6 +10777,8 @@ %1$d notification channels. Tap to manage all. + + You recently installed this app. Switch output From 19f8d244e618dd93362b39be778f881603cbd3ec Mon Sep 17 00:00:00 2001 From: Beverly Date: Tue, 19 Mar 2019 10:56:07 -0400 Subject: [PATCH 09/13] Remove wake from Settings gestures.xml Test: manual Bug: 127955764 Change-Id: Icea129fe5a7b1dbb0bc5ff799c99907a278396df --- res/xml/gestures.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/xml/gestures.xml b/res/xml/gestures.xml index f7056307826..8515bd71892 100644 --- a/res/xml/gestures.xml +++ b/res/xml/gestures.xml @@ -27,12 +27,6 @@ android:fragment="com.android.settings.gestures.AssistGestureSettings" settings:controller="com.android.settings.gestures.AssistGestureSettingsPreferenceController" /> - - Date: Mon, 18 Mar 2019 16:37:28 -0700 Subject: [PATCH 10/13] Stop using framework internal layout for timezone header. The framework version header has deviated from supportlib version long ago, and we shouldn't hack our theme to make this layout work. Instead we now have a custom layout compatible to settings theme. Fixes: 128833223 Test: visual Change-Id: I83087352240de183b0639daf844bd7f90e02056a --- res/layout/time_zone_search_header.xml | 28 +++++++++++++++++++ .../timezone/BaseTimeZoneAdapter.java | 12 ++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 res/layout/time_zone_search_header.xml diff --git a/res/layout/time_zone_search_header.xml b/res/layout/time_zone_search_header.xml new file mode 100644 index 00000000000..5c4e0ee6b71 --- /dev/null +++ b/res/layout/time_zone_search_header.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java index ff980b2ceca..66735c8a5e1 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java @@ -77,9 +77,10 @@ public class BaseTimeZoneAdapter @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - switch(viewType) { + switch (viewType) { case TYPE_HEADER: { - final View view = inflater.inflate(R.layout.preference_category_material, + final View view = inflater.inflate( + R.layout.time_zone_search_header, parent, false); return new HeaderViewHolder(view); } @@ -136,7 +137,8 @@ public class BaseTimeZoneAdapter return mShowHeader && position == 0; } - public @NonNull ArrayFilter getFilter() { + @NonNull + public ArrayFilter getFilter() { if (mFilter == null) { mFilter = new ArrayFilter(); } @@ -153,14 +155,18 @@ public class BaseTimeZoneAdapter public interface AdapterItem { CharSequence getTitle(); + CharSequence getSummary(); + String getIconText(); + String getCurrentTime(); /** * @return unique non-negative number */ long getItemId(); + String[] getSearchKeys(); } From 12f9b2a73b91de5d85aedece506dee7ef67559da Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Tue, 19 Mar 2019 13:17:09 -0700 Subject: [PATCH 11/13] Enlarge the height of collapsing button To improve the accessibility of Settings, we make the height of collapsing button set to 48dp. Fixes: 128896511 Test: rebuild Change-Id: I341196802d6d488a8adb88501901e761bc188f7b --- res/values/dimens.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/dimens.xml b/res/values/dimens.xml index f5b6e958789..ef6fc4f567a 100755 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -392,7 +392,7 @@ 4dp 16dp 16dp - 44dp + 48dp 10dp 10dp 24dp From 2f8e02fdbee3ecf7168ac9d36f3d86aa3efe4fc4 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Tue, 19 Mar 2019 14:15:37 -0700 Subject: [PATCH 12/13] Finalize text of per-profile IME settings This is a follow up CL to a recent CL [1], which updated text in the Settings app as follows: * "Keyboard & inputs" to "Keyboards" * "Input assistance" to "Tools" For consystency, this CL updates the header text for the work-profile specific enties in Languages & input from Work profile input & assistance to Work profile keyboards & tools. Other than that, there is no user-visible behavior change. [1]: Idbf1e3a8b0c7116271e424548ef37340199eaea2 5b27d18e38f7d684f16aef84b1dfc5d4808cdc07 Bug: 124049901 Fix: 123314089 Test: Manually verified as follows. 1. Build and flash an AOSP build 2. Install Test DPC 3. Open Settings -> System -> Languages & input 4. Make sure that the new category header for work-profile specific entries is now shown as "Work profile keyboards & tools" Change-Id: Ib6470debe9be29ec0985cf1e3eb31d232c6b3880 --- res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 003d611c6cf..631d9120cad 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4525,8 +4525,8 @@ Keyboard shortcuts helper Display available shortcuts - - Work profile input & assistance + + Work profile keyboards & tools Virtual keyboard for work From e366f02712dc11a252567e983da7a5397af117fc Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Tue, 19 Mar 2019 13:08:10 -0700 Subject: [PATCH 13/13] Make a public activity for zen access details. Bug: 128547723 Test: manual Change-Id: I0546cbb37c2be50bff44396fa2874d9eb294813c --- AndroidManifest.xml | 13 +++++++++++++ src/com/android/settings/Settings.java | 1 + .../settings/core/gateway/SettingsGateway.java | 2 ++ 3 files changed, 16 insertions(+) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 4bb1a8c530a..5b786813069 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2389,6 +2389,19 @@ android:value="com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails" /> + + + + + + + + +