From 934278e050c465c8220fef7a18db56916f3c30e3 Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Wed, 22 Aug 2018 11:41:13 -0700 Subject: [PATCH 01/12] Fixed NPE that crashes Settings. It happens due to a race condition when the AutofillDeveloperSettingsObserver is triggered by a settings change while the underlying preferences activity is being finished. Fixes: 113030661 Test: echo 'could not reproduce this issue' Test: runtest --path \ packages/apps/Settings/tests/unit/src/com/android/settings/core/\ PreferenceControllerContractTest.java Test: atest AutofillResetOptionsPreferenceControllerTest \ AutofillLoggingLevelPreferenceControllerTest Change-Id: I761195df13ac10705b9ce6b0c7546ec66a85d3ef --- .../AutofillLoggingLevelPreferenceController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java b/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java index a22295c557f..8618bc542c8 100644 --- a/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java +++ b/src/com/android/settings/development/autofill/AutofillLoggingLevelPreferenceController.java @@ -19,6 +19,7 @@ package com.android.settings.development.autofill; import android.content.Context; import android.content.res.Resources; import android.provider.Settings; +import android.util.Log; import android.view.autofill.AutofillManager; import com.android.settings.R; @@ -32,6 +33,7 @@ public final class AutofillLoggingLevelPreferenceController extends DeveloperOptionsPreferenceController implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + private static final String TAG = "AutofillLoggingLevelPreferenceController"; private static final String AUTOFILL_LOGGING_LEVEL_KEY = "autofill_logging_level"; private final String[] mListValues; @@ -73,6 +75,12 @@ public final class AutofillLoggingLevelPreferenceController } private void updateOptions() { + if (mPreference == null) { + // TODO: there should be a hook on AbstractPreferenceController where we could + // unregister mObserver and avoid this check + Log.v(TAG, "ignoring Settings update because UI is gone"); + return; + } final int level = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.AUTOFILL_LOGGING_LEVEL, AutofillManager.DEFAULT_LOGGING_LEVEL); From a2adcc57c8dcf9040cd469e3d5b2ac4bac3d3f62 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Wed, 22 Aug 2018 11:06:07 -0700 Subject: [PATCH 02/12] Fix special access summary text Use ApplicationState to count number of apps using unrestricted_data. Change-Id: I083ff50e3e516536c87afa71d786b22e83d9a498 Fixes: 69313992 Test: robotests --- res/xml/app_and_notification.xml | 12 +- .../AppAndNotificationDashboardFragment.java | 11 +- .../applications/AppStateBaseBridge.java | 7 +- .../SpecialAppAccessPreferenceController.java | 139 +++++++++++++++--- .../settings/datausage/DataSaverBackend.java | 21 +-- ...pAndNotificationDashboardFragmentTest.java | 65 -------- ...cialAppAccessPreferenceControllerTest.java | 60 +++++--- .../shadow/ShadowApplicationsState.java | 32 ++++ 8 files changed, 217 insertions(+), 130 deletions(-) delete mode 100644 tests/robotests/src/com/android/settings/applications/AppAndNotificationDashboardFragmentTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowApplicationsState.java diff --git a/res/xml/app_and_notification.xml b/res/xml/app_and_notification.xml index 5a55ba5dab0..c15df75afdc 100644 --- a/res/xml/app_and_notification.xml +++ b/res/xml/app_and_notification.xml @@ -34,18 +34,18 @@ android:title="@string/applications_settings" android:key="all_app_info" android:fragment="com.android.settings.applications.manageapplications.ManageApplications" - android:order="20" /> + android:order="20"/> + android:order="-190"/> + android:order="10"/> - + + android:targetClass="com.android.cellbroadcastreceiver.CellBroadcastSettings"/> + settings:controller="com.android.settings.applications.SpecialAppAccessPreferenceController"/> diff --git a/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java b/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java index 3eaed2dbdf2..9ac3ecc4c9e 100644 --- a/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java +++ b/src/com/android/settings/applications/AppAndNotificationDashboardFragment.java @@ -21,6 +21,8 @@ import android.app.Application; import android.content.Context; import android.provider.SearchIndexableResource; +import androidx.fragment.app.Fragment; + import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.dashboard.DashboardFragment; @@ -33,8 +35,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import androidx.fragment.app.Fragment; - @SearchIndexable public class AppAndNotificationDashboardFragment extends DashboardFragment { @@ -60,6 +60,12 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment { return R.xml.app_and_notification; } + @Override + public void onAttach(Context context) { + super.onAttach(context); + use(SpecialAppAccessPreferenceController.class).setSession(getSettingsLifecycle()); + } + @Override protected List createPreferenceControllers(Context context) { final Activity activity = getActivity(); @@ -77,7 +83,6 @@ public class AppAndNotificationDashboardFragment extends DashboardFragment { final List controllers = new ArrayList<>(); controllers.add(new EmergencyBroadcastPreferenceController(context, "app_and_notif_cell_broadcast_settings")); - controllers.add(new SpecialAppAccessPreferenceController(context)); controllers.add(new RecentAppsPreferenceController(context, app, host)); return controllers; } diff --git a/src/com/android/settings/applications/AppStateBaseBridge.java b/src/com/android/settings/applications/AppStateBaseBridge.java index 2329b4469e6..1a39483af9a 100644 --- a/src/com/android/settings/applications/AppStateBaseBridge.java +++ b/src/com/android/settings/applications/AppStateBaseBridge.java @@ -45,7 +45,7 @@ public abstract class AppStateBaseBridge implements ApplicationsState.Callbacks // the same time as us as well. mHandler = new BackgroundHandler(mAppState != null ? mAppState.getBackgroundLooper() : Looper.getMainLooper()); - mMainHandler = new MainHandler(); + mMainHandler = new MainHandler(Looper.getMainLooper()); } public void resume() { @@ -106,11 +106,16 @@ public abstract class AppStateBaseBridge implements ApplicationsState.Callbacks } protected abstract void loadAllExtraInfo(); + protected abstract void updateExtraInfo(AppEntry app, String pkg, int uid); private class MainHandler extends Handler { private static final int MSG_INFO_UPDATED = 1; + public MainHandler(Looper looper) { + super(looper); + } + @Override public void handleMessage(Message msg) { switch (msg.what) { diff --git a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java b/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java index 16a6cab5d98..a395f9833f0 100644 --- a/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java +++ b/src/com/android/settings/applications/SpecialAppAccessPreferenceController.java @@ -13,43 +13,140 @@ */ package com.android.settings.applications; +import android.app.Application; import android.content.Context; -import com.android.settings.R; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settings.datausage.DataSaverBackend; -import com.android.settingslib.core.AbstractPreferenceController; - +import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; -public class SpecialAppAccessPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.datausage.AppStateDataUsageBridge; +import com.android.settings.datausage.DataSaverBackend; +import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnDestroy; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; - private static final String KEY_SPECIAL_ACCESS = "special_access"; +import java.util.ArrayList; - private DataSaverBackend mDataSaverBackend; +public class SpecialAppAccessPreferenceController extends BasePreferenceController implements + AppStateBaseBridge.Callback, ApplicationsState.Callbacks, LifecycleObserver, OnStart, + OnStop, OnDestroy { - public SpecialAppAccessPreferenceController(Context context) { - super(context); + @VisibleForTesting + ApplicationsState.Session mSession; + + private final ApplicationsState mApplicationsState; + private final AppStateDataUsageBridge mDataUsageBridge; + private final DataSaverBackend mDataSaverBackend; + + private Preference mPreference; + private boolean mExtraLoaded; + + + public SpecialAppAccessPreferenceController(Context context, String key) { + super(context, key); + mApplicationsState = ApplicationsState.getInstance( + (Application) context.getApplicationContext()); + mDataSaverBackend = new DataSaverBackend(context); + mDataUsageBridge = new AppStateDataUsageBridge(mApplicationsState, this, mDataSaverBackend); + } + + public void setSession(Lifecycle lifecycle) { + mSession = mApplicationsState.newSession(this, lifecycle); } @Override - public boolean isAvailable() { - return true; + public int getAvailabilityStatus() { + return AVAILABLE_UNSEARCHABLE; } @Override - public String getPreferenceKey() { - return KEY_SPECIAL_ACCESS; + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void onStart() { + mDataUsageBridge.resume(); + } + + @Override + public void onStop() { + mDataUsageBridge.pause(); + } + + @Override + public void onDestroy() { + mDataUsageBridge.release(); } @Override public void updateState(Preference preference) { - if (mDataSaverBackend == null) { - mDataSaverBackend = new DataSaverBackend(mContext); - } - final int count = mDataSaverBackend.getWhitelistedCount(); - preference.setSummary(mContext.getResources().getQuantityString( - R.plurals.special_access_summary, count, count)); + updateSummary(); } + + @Override + public void onExtraInfoUpdated() { + mExtraLoaded = true; + updateSummary(); + } + + private void updateSummary() { + if (!mExtraLoaded || mPreference == null) { + return; + } + + final ArrayList allApps = mSession.getAllApps(); + int count = 0; + for (ApplicationsState.AppEntry entry : allApps) { + if (!ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(entry)) { + continue; + } + if (entry.extraInfo != null && ((AppStateDataUsageBridge.DataUsageState) + entry.extraInfo).isDataSaverWhitelisted) { + count++; + } + } + mPreference.setSummary(mContext.getResources().getQuantityString( + R.plurals.special_access_summary, count, count)); + } + + @Override + public void onRunningStateChanged(boolean running) { + } + + @Override + public void onPackageListChanged() { + } + + @Override + public void onRebuildComplete(ArrayList apps) { + } + + @Override + public void onPackageIconChanged() { + } + + @Override + public void onPackageSizeChanged(String packageName) { + } + + @Override + public void onAllSizesComputed() { + } + + @Override + public void onLauncherInfoChanged() { + } + + @Override + public void onLoadEntriesCompleted() { + } + } diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java index b59da9d7ff8..ed2e6c9195c 100644 --- a/src/com/android/settings/datausage/DataSaverBackend.java +++ b/src/com/android/settings/datausage/DataSaverBackend.java @@ -95,19 +95,10 @@ public class DataSaverBackend { return mUidPolicies.get(uid, POLICY_NONE) == POLICY_ALLOW_METERED_BACKGROUND; } - public int getWhitelistedCount() { - int count = 0; - loadWhitelist(); - for (int i = 0; i < mUidPolicies.size(); i++) { - if (mUidPolicies.valueAt(i) == POLICY_ALLOW_METERED_BACKGROUND) { - count++; - } - } - return count; - } - private void loadWhitelist() { - if (mWhitelistInitialized) return; + if (mWhitelistInitialized) { + return; + } for (int uid : mPolicyManager.getUidsWithPolicy(POLICY_ALLOW_METERED_BACKGROUND)) { mUidPolicies.put(uid, POLICY_ALLOW_METERED_BACKGROUND); @@ -135,7 +126,9 @@ public class DataSaverBackend { } private void loadBlacklist() { - if (mBlacklistInitialized) return; + if (mBlacklistInitialized) { + return; + } for (int uid : mPolicyManager.getUidsWithPolicy(POLICY_REJECT_METERED_BACKGROUND)) { mUidPolicies.put(uid, POLICY_REJECT_METERED_BACKGROUND); } @@ -212,7 +205,9 @@ public class DataSaverBackend { public interface Listener { void onDataSaverChanged(boolean isDataSaving); + void onWhitelistStatusChanged(int uid, boolean isWhitelisted); + void onBlacklistStatusChanged(int uid, boolean isBlacklisted); } } diff --git a/tests/robotests/src/com/android/settings/applications/AppAndNotificationDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/applications/AppAndNotificationDashboardFragmentTest.java deleted file mode 100644 index c332c064da3..00000000000 --- a/tests/robotests/src/com/android/settings/applications/AppAndNotificationDashboardFragmentTest.java +++ /dev/null @@ -1,65 +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.applications; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import android.content.Context; -import android.os.UserManager; - -import com.android.settings.notification.EmergencyBroadcastPreferenceController; -import com.android.settings.testutils.SettingsRobolectricTestRunner; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; - -import java.util.List; - -@RunWith(SettingsRobolectricTestRunner.class) -public class AppAndNotificationDashboardFragmentTest { - - @Test - @Config(shadows = {ShadowEmergencyBroadcastPreferenceController.class}) - public void getNonIndexableKeys_shouldIncludeSpecialAppAccess() { - final Context context = spy(RuntimeEnvironment.application); - UserManager manager = mock(UserManager.class); - when(manager.isAdminUser()).thenReturn(true); - when(context.getSystemService(Context.USER_SERVICE)).thenReturn(manager); - final List niks = AppAndNotificationDashboardFragment.SEARCH_INDEX_DATA_PROVIDER - .getNonIndexableKeys(context); - - assertThat(niks).contains( - new SpecialAppAccessPreferenceController(context).getPreferenceKey()); - } - - @Implements(EmergencyBroadcastPreferenceController.class) - public static class ShadowEmergencyBroadcastPreferenceController { - - @Implementation - public boolean isAvailable() { - return true; - } - } -} diff --git a/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java index 224a8f9561b..b6429151b67 100644 --- a/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/SpecialAppAccessPreferenceControllerTest.java @@ -16,15 +16,25 @@ package com.android.settings.applications; +import static com.android.settings.core.BasePreferenceController.AVAILABLE_UNSEARCHABLE; + import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.verify; + +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.pm.ApplicationInfo; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; import com.android.settings.R; -import com.android.settings.datausage.DataSaverBackend; +import com.android.settings.datausage.AppStateDataUsageBridge; import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowApplicationsState; +import com.android.settings.testutils.shadow.ShadowUserManager; +import com.android.settingslib.applications.ApplicationsState; import org.junit.Before; import org.junit.Test; @@ -32,48 +42,56 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; -import org.robolectric.util.ReflectionHelpers; +import org.robolectric.annotation.Config; -import androidx.preference.Preference; +import java.util.ArrayList; @RunWith(SettingsRobolectricTestRunner.class) +@Config(shadows = {ShadowUserManager.class, ShadowApplicationsState.class}) public class SpecialAppAccessPreferenceControllerTest { private Context mContext; @Mock - private DataSaverBackend mBackend; + private ApplicationsState.Session mSession; @Mock - private Preference mPreference; + private PreferenceScreen mScreen; private SpecialAppAccessPreferenceController mController; + private Preference mPreference; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mController = new SpecialAppAccessPreferenceController(mContext); - ReflectionHelpers.setField(mController, "mDataSaverBackend", mBackend); + ShadowUserManager.getShadow().setProfileIdsWithDisabled(new int[]{0}); + mController = new SpecialAppAccessPreferenceController(mContext, "test_key"); + mPreference = new Preference(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); + + mController.mSession = mSession; } @Test - public void isAvailable_shouldAlwaysReturnTrue() { - assertThat(mController.isAvailable()).isTrue(); + public void getAvailabilityState_unsearchable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE_UNSEARCHABLE); } @Test public void updateState_shouldSetSummary() { - when(mBackend.getWhitelistedCount()).thenReturn(0); + final ArrayList apps = new ArrayList<>(); + final ApplicationsState.AppEntry entry = mock(ApplicationsState.AppEntry.class); + entry.hasLauncherEntry = true; + entry.info = new ApplicationInfo(); + entry.extraInfo = new AppStateDataUsageBridge.DataUsageState( + true /* whitelisted */, false /* blacklisted */); + apps.add(entry); + when(mSession.getAllApps()).thenReturn(apps); - mController.updateState(mPreference); + mController.displayPreference(mScreen); + mController.onExtraInfoUpdated(); - verify(mPreference).setSummary(mContext.getResources() - .getQuantityString(R.plurals.special_access_summary, 0, 0)); - - when(mBackend.getWhitelistedCount()).thenReturn(1); - - mController.updateState(mPreference); - - verify(mPreference).setSummary(mContext.getResources() - .getQuantityString(R.plurals.special_access_summary, 1, 1)); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getResources().getQuantityString( + R.plurals.special_access_summary, 1, 1)); } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowApplicationsState.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowApplicationsState.java new file mode 100644 index 00000000000..3ee4fcb1647 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowApplicationsState.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2018 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.testutils.shadow; + +import android.os.Looper; + +import com.android.settingslib.applications.ApplicationsState; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(ApplicationsState.class) +public class ShadowApplicationsState { + @Implementation + public Looper getBackgroundLooper() { + return Looper.getMainLooper(); + } +} From a9fa4c1d0572a34ee84abd51007b6262bc1d2c76 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Wed, 22 Aug 2018 17:45:23 -0700 Subject: [PATCH 03/12] Hook up feature flag to mobile network controller Send intent to phone process if the flag is false. Bug: 113069948 Test: RunSettingsRoboTests Change-Id: Ie9726470e718144557f318fe7ea28e863d63679c --- res/xml/network_and_internet.xml | 4 --- .../android/settings/core/FeatureFlags.java | 1 + .../MobileNetworkPreferenceController.java | 26 +++++++++++++++- ...MobileNetworkPreferenceControllerTest.java | 31 +++++++++++++++++-- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/res/xml/network_and_internet.xml b/res/xml/network_and_internet.xml index 0d130e06b81..8e36e919220 100644 --- a/res/xml/network_and_internet.xml +++ b/res/xml/network_and_internet.xml @@ -42,10 +42,6 @@ settings:keywords="@string/keywords_more_mobile_networks" settings:userRestriction="no_config_mobile_networks" settings:useAdminDisabledSummary="true"> - mLifecycle; mLifecycle = new Lifecycle(mLifecycleOwner); when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager); + mPreference = new Preference(mContext); + mPreference.setKey(MobileNetworkPreferenceController.KEY_MOBILE_NETWORK_SETTINGS); } @Test @@ -173,4 +186,18 @@ public class MobileNetworkPreferenceControllerTest { mController.updateState(mPreference); assertThat(mPreference.isEnabled()).isFalse(); } + + @Test + public void handlePreferenceTreeClick_mobileFeatureDisabled_sendIntent() { + mController = new MobileNetworkPreferenceController(mContext); + FeatureFlagUtils.setEnabled(mContext, FeatureFlags.MOBILE_NETWORK_V2, false); + ArgumentCaptor argument = ArgumentCaptor.forClass(Intent.class); + + mController.handlePreferenceTreeClick(mPreference); + + verify(mContext).startActivity(argument.capture()); + final ComponentName componentName = argument.getValue().getComponent(); + assertThat(componentName.getPackageName()).isEqualTo(MOBILE_NETWORK_PACKAGE); + assertThat(componentName.getClassName()).isEqualTo(MOBILE_NETWORK_CLASS); + } } From ab1bc299d8d38d44f72f493b0bd6090e5e4b6d54 Mon Sep 17 00:00:00 2001 From: hughchen Date: Mon, 20 Aug 2018 17:22:32 +0800 Subject: [PATCH 04/12] Use BluetoothDevice.ACCESS_* instead of CachedBluetoothDevice.ACCESS_* 1. Use BluetoothDevice.ACCESS_* instead of CachedBluetoothDevice.ACCESS_* 2. Use BluetoothDevice.setPhonebookAccessPermission() directly. 3. Use BluetoothDevice.setMessageAccessPermission() directly. 4. Use BluetoothDevice.getPhonebookAccessPermission() directly. 5. Use BluetoothDevice.getMessageAccessPermission() directly. 6. Use BluetoothDevice.getSimAccessPermission() directly. Bug: 112517004 Test: make -j42 RunSettingsRoboTests Change-Id: Ibe6b207b891b9bd2b328a2e2c7264a9a78cb498f --- .../BluetoothDetailsProfilesController.java | 42 ++++++------- .../bluetooth/BluetoothPermissionRequest.java | 24 ++++---- ...luetoothDetailsProfilesControllerTest.java | 21 ++++--- .../shadow/ShadowBluetoothDevice.java | 59 +++++++++++++++++++ 4 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowBluetoothDevice.java diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java index cd452817d0e..20e702a3eee 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java @@ -98,11 +98,11 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll BluetoothDevice device = mCachedDevice.getDevice(); profilePref.setEnabled(!mCachedDevice.isBusy()); if (profile instanceof MapProfile) { - profilePref.setChecked(mCachedDevice.getMessagePermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); + profilePref.setChecked(device.getMessageAccessPermission() + == BluetoothDevice.ACCESS_ALLOWED); } else if (profile instanceof PbapServerProfile) { - profilePref.setChecked(mCachedDevice.getPhonebookPermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED); + profilePref.setChecked(device.getPhonebookAccessPermission() + == BluetoothDevice.ACCESS_ALLOWED); } else if (profile instanceof PanProfile) { profilePref.setChecked(profile.getConnectionStatus(device) == BluetoothProfile.STATE_CONNECTED); @@ -130,31 +130,31 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll /** * Helper method to enable a profile for a device. */ - private void enableProfile(LocalBluetoothProfile profile, BluetoothDevice device, - SwitchPreference profilePref) { + private void enableProfile(LocalBluetoothProfile profile) { + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); if (profile instanceof PbapServerProfile) { - mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); + bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); // We don't need to do the additional steps below for this profile. return; } if (profile instanceof MapProfile) { - mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); + bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); } - profile.setPreferred(device, true); + profile.setPreferred(bluetoothDevice, true); mCachedDevice.connectProfile(profile); } /** * Helper method to disable a profile for a device */ - private void disableProfile(LocalBluetoothProfile profile, BluetoothDevice device, - SwitchPreference profilePref) { + private void disableProfile(LocalBluetoothProfile profile) { + final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice(); mCachedDevice.disconnect(profile); - profile.setPreferred(device, false); + profile.setPreferred(bluetoothDevice, false); if (profile instanceof MapProfile) { - mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); + bluetoothDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); } else if (profile instanceof PbapServerProfile) { - mCachedDevice.setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); + bluetoothDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } @@ -175,11 +175,10 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll } } SwitchPreference profilePref = (SwitchPreference) preference; - BluetoothDevice device = mCachedDevice.getDevice(); if (profilePref.isChecked()) { - enableProfile(profile, device, profilePref); + enableProfile(profile); } else { - disableProfile(profile, device, profilePref); + disableProfile(profile); } refreshProfilePreference(profilePref, profile); return true; @@ -191,17 +190,18 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll */ private List getProfiles() { List result = mCachedDevice.getConnectableProfiles(); + final BluetoothDevice device = mCachedDevice.getDevice(); - final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); + final int pbapPermission = device.getPhonebookAccessPermission(); // Only provide PBAP cabability if the client device has requested PBAP. - if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (pbapPermission != BluetoothDevice.ACCESS_UNKNOWN) { final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); result.add(psp); } final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); - final int mapPermission = mCachedDevice.getMessagePermissionChoice(); - if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { + final int mapPermission = device.getMessageAccessPermission(); + if (mapPermission != BluetoothDevice.ACCESS_UNKNOWN) { result.add(mapProfile); } diff --git a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java index c452957bc14..c5f62b8b4d6 100644 --- a/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java +++ b/src/com/android/settings/bluetooth/BluetoothPermissionRequest.java @@ -237,42 +237,42 @@ public final class BluetoothPermissionRequest extends BroadcastReceiver { String intentName = BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY; if (mRequestType == BluetoothDevice.REQUEST_TYPE_PHONEBOOK_ACCESS) { - int phonebookPermission = cachedDevice.getPhonebookPermissionChoice(); + int phonebookPermission = mDevice.getPhonebookAccessPermission(); - if (phonebookPermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (phonebookPermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (phonebookPermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (phonebookPermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (phonebookPermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad phonebookPermission: " + phonebookPermission); } } else if (mRequestType == BluetoothDevice.REQUEST_TYPE_MESSAGE_ACCESS) { - int messagePermission = cachedDevice.getMessagePermissionChoice(); + int messagePermission = mDevice.getMessageAccessPermission(); - if (messagePermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (messagePermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (messagePermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (messagePermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (messagePermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (messagePermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { Log.e(TAG, "Bad messagePermission: " + messagePermission); } } else if(mRequestType == BluetoothDevice.REQUEST_TYPE_SIM_ACCESS) { - int simPermission = cachedDevice.getSimPermissionChoice(); + int simPermission = mDevice.getSimAccessPermission(); - if (simPermission == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (simPermission == BluetoothDevice.ACCESS_UNKNOWN) { // Leave 'processed' as false. - } else if (simPermission == CachedBluetoothDevice.ACCESS_ALLOWED) { + } else if (simPermission == BluetoothDevice.ACCESS_ALLOWED) { sendReplyIntentToReceiver(true); processed = true; - } else if (simPermission == CachedBluetoothDevice.ACCESS_REJECTED) { + } else if (simPermission == BluetoothDevice.ACCESS_REJECTED) { sendReplyIntentToReceiver(false); processed = true; } else { diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java index 62414e4b52f..848cdffe982 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsProfilesControllerTest.java @@ -31,8 +31,8 @@ import android.content.Context; import com.android.settings.R; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.shadow.SettingsShadowBluetoothDevice; +import com.android.settings.testutils.shadow.ShadowBluetoothDevice; import com.android.settingslib.bluetooth.A2dpProfile; -import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfile; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; @@ -55,7 +55,7 @@ import androidx.preference.PreferenceCategory; import androidx.preference.SwitchPreference; @RunWith(SettingsRobolectricTestRunner.class) -@Config(shadows = SettingsShadowBluetoothDevice.class) +@Config(shadows = {SettingsShadowBluetoothDevice.class, ShadowBluetoothDevice.class}) public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsControllerTestBase { private BluetoothDetailsProfilesController mController; @@ -290,8 +290,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont @Test public void pbapProfileStartsEnabled() { setupDevice(makeDefaultDeviceConfig()); - when(mCachedDevice.getPhonebookPermissionChoice()) - .thenReturn(CachedBluetoothDevice.ACCESS_ALLOWED); + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); when(psp.toString()).thenReturn(PbapServerProfile.NAME); @@ -306,14 +305,14 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); - verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); + assertThat(mDevice.getPhonebookAccessPermission()) + .isEqualTo(BluetoothDevice.ACCESS_REJECTED); } @Test public void pbapProfileStartsDisabled() { setupDevice(makeDefaultDeviceConfig()); - when(mCachedDevice.getPhonebookPermissionChoice()) - .thenReturn(CachedBluetoothDevice.ACCESS_REJECTED); + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); PbapServerProfile psp = mock(PbapServerProfile.class); when(psp.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_pbap); when(psp.toString()).thenReturn(PbapServerProfile.NAME); @@ -328,7 +327,8 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); - verify(mCachedDevice).setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); + assertThat(mDevice.getPhonebookAccessPermission()) + .isEqualTo(BluetoothDevice.ACCESS_ALLOWED); } @Test @@ -338,8 +338,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont when(mapProfile.getNameResource(mDevice)).thenReturn(R.string.bluetooth_profile_map); when(mProfileManager.getMapProfile()).thenReturn(mapProfile); when(mProfileManager.getProfileByName(eq(mapProfile.toString()))).thenReturn(mapProfile); - when(mCachedDevice.getMessagePermissionChoice()) - .thenReturn(CachedBluetoothDevice.ACCESS_REJECTED); + mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); showScreen(mController); List switches = getProfileSwitches(false); assertThat(switches.size()).isEqualTo(1); @@ -349,7 +348,7 @@ public class BluetoothDetailsProfilesControllerTest extends BluetoothDetailsCont pref.performClick(); assertThat(mProfiles.getPreferenceCount()).isEqualTo(1); - verify(mCachedDevice).setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); + assertThat(mDevice.getMessageAccessPermission()).isEqualTo(BluetoothDevice.ACCESS_ALLOWED); } private A2dpProfile addMockA2dpProfile(boolean preferred, boolean supportsHighQualityAudio, diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowBluetoothDevice.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowBluetoothDevice.java new file mode 100644 index 00000000000..4cc77c3b072 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowBluetoothDevice.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 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.testutils.shadow; + +import android.bluetooth.BluetoothDevice; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(value = BluetoothDevice.class, inheritImplementationMethods = true) +public class ShadowBluetoothDevice extends org.robolectric.shadows.ShadowBluetoothDevice { + + private int mMessageAccessPermission = BluetoothDevice.ACCESS_UNKNOWN; + private int mPhonebookAccessPermission = BluetoothDevice.ACCESS_UNKNOWN; + private int mSimAccessPermission = BluetoothDevice.ACCESS_UNKNOWN; + + @Implementation + public void setMessageAccessPermission(int value) { + mMessageAccessPermission = value; + } + + @Implementation + public int getMessageAccessPermission() { + return mMessageAccessPermission; + } + + @Implementation + public void setPhonebookAccessPermission(int value) { + mPhonebookAccessPermission = value; + } + + @Implementation + public int getPhonebookAccessPermission() { + return mPhonebookAccessPermission; + } + + @Implementation + public void setSimAccessPermission(int value) { + mSimAccessPermission = value; + } + + @Implementation + public int getSimAccessPermission() { + return mSimAccessPermission; + } +} From 810496aa152fefb4969ee286dcf719c250a4e477 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Thu, 23 Aug 2018 14:27:49 +0800 Subject: [PATCH 05/12] Check getLastCustomNonConfigurationInstance() works well - make sure that StorageWizardFormatProgress runs smoothly. - confirm that mTask has the same object id when changing the screen orientation. Bug: 111151113 Test: manual Change-Id: I730c1b0f08e8a20a1aafa668fb02cd5a09fdd70a --- .../settings/deviceinfo/StorageWizardFormatProgress.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java b/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java index 287cc3f0ec6..2d94597c622 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java +++ b/src/com/android/settings/deviceinfo/StorageWizardFormatProgress.java @@ -59,7 +59,6 @@ public class StorageWizardFormatProgress extends StorageWizardBase { setHeaderText(R.string.storage_wizard_format_progress_title, getDiskShortDescription()); setBodyText(R.string.storage_wizard_format_progress_body, getDiskDescription()); - // TODO (b/111151113) : Need to check it again. mTask = (PartitionTask) getLastCustomNonConfigurationInstance(); if (mTask == null) { mTask = new PartitionTask(); @@ -69,7 +68,7 @@ public class StorageWizardFormatProgress extends StorageWizardBase { mTask.setActivity(this); } } - // TODO (b/111151113) : Need to check it again. + @Override public Object onRetainCustomNonConfigurationInstance() { return mTask; From 5b05ebb46bac06750ec682c17a1e792a4f9f60a6 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Thu, 23 Aug 2018 20:58:04 +0800 Subject: [PATCH 06/12] Check StorageWizardFormatConfirm runs smoothly - confirm that StorageWizardFormatConfirm works well after changing calling method from showAllowingStateLoss() to show(). Bug: 111150236 Test: manual Change-Id: I0ad6c54aaa2572fc5798ede900e0a6d10d2fbd35 --- .../android/settings/deviceinfo/StorageWizardFormatConfirm.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java index e09e89d92ba..c3d034f5e40 100644 --- a/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java +++ b/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java @@ -59,7 +59,6 @@ public class StorageWizardFormatConfirm extends InstrumentedDialogFragment { final StorageWizardFormatConfirm fragment = new StorageWizardFormatConfirm(); fragment.setArguments(args); - // TODO (b/111150236) : Need to check it again. fragment.show(activity.getSupportFragmentManager(), TAG_FORMAT_WARNING); } From 2c9d9ce8deed3225417b74405947aae0aa0afbb7 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 23 Aug 2018 09:25:18 -0700 Subject: [PATCH 07/12] DO NOT MERGE Fix summary text for advanced button. - the resource for the summary text is in the support library. However, the prebuilt version in the build does not contain the latest change for the summary text. Add the overlay for the summary resource here with the updated translation to remove the redundant summary prefix in the advanced button. Change-Id: Ib6db0c402d79a2d965e6060b1851d7b898ae3f94 Fixes: 112986476 Bug: 111195268 Test: manual --- res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index f159b058fba..255d4a0dfb3 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -4149,4 +4149,5 @@ "网络详情" "您的设备名称会显示在手机上的应用中。此外,当您连接到蓝牙设备或设置 WLAN 热点时,其他人可能也会看到您的设备名称。" "设备" + "%1$s%2$s" From 7915fda7eea56a8461be3025dc7a6f18cf26c15a Mon Sep 17 00:00:00 2001 From: David Sodman Date: Fri, 20 Jul 2018 13:57:41 -0700 Subject: [PATCH 08/12] Add Support for Virtual High Refresh Rate mode Add a developer options settings switch to enable a virtual HiFrequency panel mode to be able to test the SW stack with display running at 50% faster than the default refresh rate. Bug: 111549030 Test: Enable HiFrequency mode and use systrace/adb to verity Change-Id: Ibfd30ca1a14a146419233eaefa9b5095bf459adc --- res/values/strings.xml | 7 + res/xml/development_settings.xml | 5 + .../DevelopmentSettingsDashboardFragment.java | 1 + ...hFrequencyDisplayPreferenceController.java | 129 ++++++++++++++++++ ...HighFrequencyPreferenceControllerTest.java | 115 ++++++++++++++++ 5 files changed, 257 insertions(+) create mode 100644 src/com/android/settings/development/HighFrequencyDisplayPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/development/HighFrequencyPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 5bdd578e959..53abd544d50 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10071,4 +10071,11 @@ Devices + + + High Frequency Panel + + + Enable Virtual High Frequency Panel + diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index a46ff2f9611..8cc8001088b 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -359,6 +359,11 @@ android:entries="@array/overlay_display_devices_entries" android:entryValues="@array/overlay_display_devices_values" /> + + diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 5be381a04c9..54ca6ef776f 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -439,6 +439,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new TransitionAnimationScalePreferenceController(context)); controllers.add(new AnimatorDurationScalePreferenceController(context)); controllers.add(new SecondaryDisplayPreferenceController(context)); + controllers.add(new HighFrequencyDisplayPreferenceController(context)); controllers.add(new GpuViewUpdatesPreferenceController(context)); controllers.add(new HardwareLayersUpdatesPreferenceController(context)); controllers.add(new DebugGpuOverdrawPreferenceController(context)); diff --git a/src/com/android/settings/development/HighFrequencyDisplayPreferenceController.java b/src/com/android/settings/development/HighFrequencyDisplayPreferenceController.java new file mode 100644 index 00000000000..cbb8d4ce6d0 --- /dev/null +++ b/src/com/android/settings/development/HighFrequencyDisplayPreferenceController.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 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.development; + +import android.content.Context; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +public class HighFrequencyDisplayPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String HIGH_FREQUENCY_DISPLAY_KEY = "high_frequency_display_device"; + + private static final String SURFACE_FLINGER_SERVICE_KEY = "SurfaceFlinger"; + private static final String SURFACE_COMPOSER_INTERFACE_KEY = "android.ui.ISurfaceComposer"; + private static final int SURFACE_FLINGER_HIGH_FREQUENCY_DISPLAY_CODE = 1029; + + private final IBinder mSurfaceFlingerBinder; + + public HighFrequencyDisplayPreferenceController(Context context) { + super(context); + mSurfaceFlingerBinder = ServiceManager.getService(SURFACE_FLINGER_SERVICE_KEY); + } + + @Override + public String getPreferenceKey() { + return HIGH_FREQUENCY_DISPLAY_KEY; + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + Boolean isEnabled = (Boolean) newValue; + writeHighFrequencyDisplaySetting(isEnabled); + ((SwitchPreference) preference).setChecked(isEnabled); + return true; + } + + @Override + public void updateState(Preference preference) { + boolean enableHighFrequencyPanel = readHighFrequencyDisplaySetting(); + ((SwitchPreference) preference).setChecked(enableHighFrequencyPanel); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + writeHighFrequencyDisplaySetting(false); + ((SwitchPreference) mPreference).setChecked(false); + } + + @VisibleForTesting + boolean readHighFrequencyDisplaySetting() { + boolean isEnabled = false; + try { + if (mSurfaceFlingerBinder != null) { + final Parcel data = Parcel.obtain(); + final Parcel result = Parcel.obtain(); + data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY); + data.writeInt(0); + data.writeInt(0); + mSurfaceFlingerBinder.transact( + SURFACE_FLINGER_HIGH_FREQUENCY_DISPLAY_CODE, + data, result, 0); + + if (result.readInt() != 1 || result.readInt() != 1) { + isEnabled = true; + } + } + } catch (RemoteException ex) { + // intentional no-op + } + return isEnabled; + } + + @VisibleForTesting + void writeHighFrequencyDisplaySetting(boolean isEnabled) { + int multiplier; + int divisor; + + if (isEnabled) { + // 60Hz * 3/2 = 90Hz + multiplier = 2; + divisor = 3; + } else { + multiplier = 1; + divisor = 1; + } + + try { + if (mSurfaceFlingerBinder != null) { + final Parcel data = Parcel.obtain(); + data.writeInterfaceToken(SURFACE_COMPOSER_INTERFACE_KEY); + data.writeInt(multiplier); + data.writeInt(divisor); + mSurfaceFlingerBinder.transact( + SURFACE_FLINGER_HIGH_FREQUENCY_DISPLAY_CODE, + data, null, 0); + } + } catch (RemoteException ex) { + // intentional no-op + } + } +} diff --git a/tests/robotests/src/com/android/settings/development/HighFrequencyPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/HighFrequencyPreferenceControllerTest.java new file mode 100644 index 00000000000..cf8babb5114 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/HighFrequencyPreferenceControllerTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2018 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.development; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +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.content.Context; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowParcel; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +@RunWith(SettingsRobolectricTestRunner.class) +public class HighFrequencyPreferenceControllerTest { + + private Context mContext; + private SwitchPreference mPreference; + + @Mock + private PreferenceScreen mScreen; + @Mock + private IBinder mSurfaceFlingerBinder; + + private HighFrequencyDisplayPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mPreference = new SwitchPreference(mContext); + mController = spy(new HighFrequencyDisplayPreferenceController(mContext)); + ReflectionHelpers.setField(mController, "mSurfaceFlingerBinder", mSurfaceFlingerBinder); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); + mController.displayPreference(mScreen); + } + + @Test + public void onPreferenceChange_settingToggledOn_shouldWriteTrueToHighFrequencySetting() { + mController.onPreferenceChange(mPreference, true /* new value */); + + verify(mController).writeHighFrequencyDisplaySetting(true); + } + + @Test + public void onPreferenceChange_settingToggledOff_shouldWriteFalseToHighFrequencySetting() { + mController.onPreferenceChange(mPreference, false /* new value */); + + verify(mController).writeHighFrequencyDisplaySetting(false); + } + + @Test + public void updateState_settingEnabled_shouldCheckPreference() throws RemoteException { + mController.writeHighFrequencyDisplaySetting(true); + mController.updateState(mPreference); + + verify(mController).readHighFrequencyDisplaySetting(); + } + + @Test + public void updateState_settingDisabled_shouldUnCheckPreference() throws RemoteException { + mController.writeHighFrequencyDisplaySetting(true); + mController.updateState(mPreference); + + verify(mController).readHighFrequencyDisplaySetting(); + } + + @Test + public void onDeveloperOptionsSwitchDisabled_preferenceChecked_shouldTurnOffPreference() { + mController.onDeveloperOptionsSwitchDisabled(); + + verify(mController).writeHighFrequencyDisplaySetting(false); + } + + @Test + public void onDeveloperOptionsSwitchDisabled_preferenceUnchecked_shouldNotTurnOffPreference() { + mController.onDeveloperOptionsSwitchDisabled(); + + verify(mController).writeHighFrequencyDisplaySetting(false); + } +} From 32db609df178bd8991f4ffdba1a7008252120b76 Mon Sep 17 00:00:00 2001 From: Mill Chen Date: Fri, 24 Aug 2018 13:02:54 +0800 Subject: [PATCH 09/12] Remove red underline from Device name - Turn off auto-correction for normal text. Bug: 79421621 Test: make RunSettingsRoboTest Change-Id: Ie7c4ebd33073ecaac2048d8630ec7b51e706341c --- .../settings/widget/ValidatedEditTextPreference.java | 3 ++- .../widget/ValidatedEditTextPreferenceTest.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/widget/ValidatedEditTextPreference.java b/src/com/android/settings/widget/ValidatedEditTextPreference.java index 76d8bcceb94..3204ab3894a 100644 --- a/src/com/android/settings/widget/ValidatedEditTextPreference.java +++ b/src/com/android/settings/widget/ValidatedEditTextPreference.java @@ -93,7 +93,8 @@ public class ValidatedEditTextPreference extends CustomEditTextPreferenceCompat textView.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } else { - textView.setInputType(InputType.TYPE_CLASS_TEXT); + textView.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); } } diff --git a/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java index 5b332825e3e..e5cb12d4405 100644 --- a/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java @@ -131,4 +131,16 @@ public class ValidatedEditTextPreferenceTest { & (InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT)) .isNotEqualTo(0); } + + @Test + public void bindViewHolder_isNotPassword_shouldNotAutoCorrectText() { + final TextView textView = spy(new TextView(RuntimeEnvironment.application)); + when(mViewHolder.findViewById(android.R.id.summary)).thenReturn(textView); + + mPreference.setIsSummaryPassword(false); + mPreference.onBindViewHolder(mViewHolder); + + assertThat(textView.getInputType()).isEqualTo( + InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_CLASS_TEXT); + } } From 5d7ebbf963822f69197ba5a684669c7a2536d554 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 23 Aug 2018 17:34:55 -0700 Subject: [PATCH 10/12] Refresh conditions only when it changes. - Instead of force refresh when user tap action button, we not wait until state changes for each conditional cards. Test: robotests Change-Id: I2eca59a06b8cb332b7b99f017baefb3d5b53234b --- .../BackgroundDataConditionController.java | 12 +++-- .../conditional/ConditionManager.java | 8 +-- .../NightDisplayConditionController.java | 9 +++- .../WorkModeConditionController.java | 32 +++++++++-- ...BackgroundDataConditionControllerTest.java | 19 ++++++- .../NightDisplayConditionControllerTest.java | 54 +++++++++++++++++++ .../WorkModeConditionControllerTest.java | 8 ++- 7 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/homepage/conditional/NightDisplayConditionControllerTest.java diff --git a/src/com/android/settings/homepage/conditional/BackgroundDataConditionController.java b/src/com/android/settings/homepage/conditional/BackgroundDataConditionController.java index d7c06987e74..0dc3cf124f9 100644 --- a/src/com/android/settings/homepage/conditional/BackgroundDataConditionController.java +++ b/src/com/android/settings/homepage/conditional/BackgroundDataConditionController.java @@ -28,9 +28,14 @@ public class BackgroundDataConditionController implements ConditionalCardControl static final int ID = Objects.hash("BackgroundDataConditionController"); private final Context mAppContext; + private final ConditionManager mConditionManager; + private final NetworkPolicyManager mNetworkPolicyManager; - public BackgroundDataConditionController(Context appContext) { + public BackgroundDataConditionController(Context appContext, ConditionManager manager) { mAppContext = appContext; + mConditionManager = manager; + mNetworkPolicyManager = + (NetworkPolicyManager) appContext.getSystemService(Context.NETWORK_POLICY_SERVICE); } @Override @@ -40,7 +45,7 @@ public class BackgroundDataConditionController implements ConditionalCardControl @Override public boolean isDisplayable() { - return NetworkPolicyManager.from(mAppContext).getRestrictBackground(); + return mNetworkPolicyManager.getRestrictBackground(); } @Override @@ -50,7 +55,8 @@ public class BackgroundDataConditionController implements ConditionalCardControl @Override public void onActionClick() { - NetworkPolicyManager.from(mAppContext).setRestrictBackground(false); + mNetworkPolicyManager.setRestrictBackground(false); + mConditionManager.onConditionChanged(); } @Override diff --git a/src/com/android/settings/homepage/conditional/ConditionManager.java b/src/com/android/settings/homepage/conditional/ConditionManager.java index d6e50123a52..d1c9a27771c 100644 --- a/src/com/android/settings/homepage/conditional/ConditionManager.java +++ b/src/com/android/settings/homepage/conditional/ConditionManager.java @@ -100,7 +100,6 @@ public class ConditionManager { */ public void onActionClick(long id) { getController(id).onActionClick(); - onConditionChanged(); } /** @@ -155,15 +154,16 @@ public class ConditionManager { private void initCandidates() { // Initialize controllers first. mCardControllers.add(new AirplaneModeConditionController(mAppContext, this /* manager */)); - mCardControllers.add(new BackgroundDataConditionController(mAppContext)); + mCardControllers.add( + new BackgroundDataConditionController(mAppContext, this /* manager */)); mCardControllers.add(new BatterySaverConditionController(mAppContext, this /* manager */)); mCardControllers.add(new CellularDataConditionController(mAppContext, this /* manager */)); mCardControllers.add(new DndConditionCardController(mAppContext, this /* manager */)); mCardControllers.add(new HotspotConditionController(mAppContext, this /* manager */)); - mCardControllers.add(new NightDisplayConditionController(mAppContext)); + mCardControllers.add(new NightDisplayConditionController(mAppContext, this /* manager */)); mCardControllers.add(new RingerVibrateConditionController(mAppContext, this /* manager */)); mCardControllers.add(new RingerMutedConditionController(mAppContext, this /* manager */)); - mCardControllers.add(new WorkModeConditionController(mAppContext)); + mCardControllers.add(new WorkModeConditionController(mAppContext, this /* manager */)); // Initialize ui model later. UI model depends on controller. mCandidates.add(new AirplaneModeConditionCard(mAppContext)); diff --git a/src/com/android/settings/homepage/conditional/NightDisplayConditionController.java b/src/com/android/settings/homepage/conditional/NightDisplayConditionController.java index 428fe488989..b4816f15a05 100644 --- a/src/com/android/settings/homepage/conditional/NightDisplayConditionController.java +++ b/src/com/android/settings/homepage/conditional/NightDisplayConditionController.java @@ -30,10 +30,12 @@ public class NightDisplayConditionController implements ConditionalCardControlle ColorDisplayController.Callback { static final int ID = Objects.hash("NightDisplayConditionController"); + private final ConditionManager mConditionManager; private final ColorDisplayController mController; - public NightDisplayConditionController(Context appContext) { + public NightDisplayConditionController(Context appContext, ConditionManager manager) { mController = new ColorDisplayController(appContext); + mConditionManager = manager; } @Override @@ -69,4 +71,9 @@ public class NightDisplayConditionController implements ConditionalCardControlle public void stopMonitoringStateChange() { mController.setListener(null); } + + @Override + public void onActivated(boolean activated) { + mConditionManager.onConditionChanged(); + } } diff --git a/src/com/android/settings/homepage/conditional/WorkModeConditionController.java b/src/com/android/settings/homepage/conditional/WorkModeConditionController.java index 1bd227a0912..033a6a86070 100644 --- a/src/com/android/settings/homepage/conditional/WorkModeConditionController.java +++ b/src/com/android/settings/homepage/conditional/WorkModeConditionController.java @@ -16,11 +16,14 @@ package com.android.settings.homepage.conditional; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.UserInfo; import android.os.UserHandle; import android.os.UserManager; +import android.text.TextUtils; import com.android.settings.Settings; @@ -31,13 +34,25 @@ public class WorkModeConditionController implements ConditionalCardController { static final int ID = Objects.hash("WorkModeConditionController"); + private static final IntentFilter FILTER = new IntentFilter(); + + static { + FILTER.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE); + FILTER.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); + } + private final Context mAppContext; private final UserManager mUm; + private final ConditionManager mConditionManager; + private final Receiver mReceiver; + private UserHandle mUserHandle; - public WorkModeConditionController(Context appContext) { + public WorkModeConditionController(Context appContext, ConditionManager manager) { mAppContext = appContext; mUm = mAppContext.getSystemService(UserManager.class); + mConditionManager = manager; + mReceiver = new Receiver(); } @Override @@ -66,12 +81,12 @@ public class WorkModeConditionController implements ConditionalCardController { @Override public void startMonitoringStateChange() { - + mAppContext.registerReceiver(mReceiver, FILTER); } @Override public void stopMonitoringStateChange() { - + mAppContext.unregisterReceiver(mReceiver); } private void updateUserHandle() { @@ -87,4 +102,15 @@ public class WorkModeConditionController implements ConditionalCardController { } } } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + || TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)) { + mConditionManager.onConditionChanged(); + } + } + } } diff --git a/tests/robotests/src/com/android/settings/homepage/conditional/BackgroundDataConditionControllerTest.java b/tests/robotests/src/com/android/settings/homepage/conditional/BackgroundDataConditionControllerTest.java index 4ad7e6dee7b..e3db59b0853 100644 --- a/tests/robotests/src/com/android/settings/homepage/conditional/BackgroundDataConditionControllerTest.java +++ b/tests/robotests/src/com/android/settings/homepage/conditional/BackgroundDataConditionControllerTest.java @@ -22,28 +22,37 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.content.Intent; +import android.net.NetworkPolicyManager; import com.android.settings.Settings; -import com.android.settings.homepage.conditional.BackgroundDataConditionController; import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; @RunWith(SettingsRobolectricTestRunner.class) public class BackgroundDataConditionControllerTest { + + @Mock + private ConditionManager mConditionManager; + @Mock + private NetworkPolicyManager mNetworkPolicyManager; private Context mContext; private BackgroundDataConditionController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + ShadowApplication.getInstance().setSystemService(Context.NETWORK_POLICY_SERVICE, + mNetworkPolicyManager); mContext = spy(RuntimeEnvironment.application); - mController = new BackgroundDataConditionController(mContext); + mController = new BackgroundDataConditionController(mContext, mConditionManager); } @Test @@ -56,4 +65,10 @@ public class BackgroundDataConditionControllerTest { assertThat(intent.getComponent().getClassName()).isEqualTo( Settings.DataUsageSummaryActivity.class.getName()); } + + @Test + public void onActionClick_shouldRefreshCondition() { + mController.onActionClick(); + verify(mConditionManager).onConditionChanged(); + } } diff --git a/tests/robotests/src/com/android/settings/homepage/conditional/NightDisplayConditionControllerTest.java b/tests/robotests/src/com/android/settings/homepage/conditional/NightDisplayConditionControllerTest.java new file mode 100644 index 00000000000..130df90c696 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/conditional/NightDisplayConditionControllerTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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.conditional; + +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +public class NightDisplayConditionControllerTest { + + @Mock + private ConditionManager mConditionManager; + + private Context mContext; + private NightDisplayConditionController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = new NightDisplayConditionController(mContext, mConditionManager); + } + + @Test + public void onActivated_shouldUpdateCondition() { + mController.onActivated(true); + + verify(mConditionManager).onConditionChanged(); + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/conditional/WorkModeConditionControllerTest.java b/tests/robotests/src/com/android/settings/homepage/conditional/WorkModeConditionControllerTest.java index 52c9ffedbf0..c993e68923d 100644 --- a/tests/robotests/src/com/android/settings/homepage/conditional/WorkModeConditionControllerTest.java +++ b/tests/robotests/src/com/android/settings/homepage/conditional/WorkModeConditionControllerTest.java @@ -24,24 +24,28 @@ import android.content.ComponentName; import android.content.Context; import com.android.settings.Settings; -import com.android.settings.homepage.conditional.WorkModeConditionController; import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; @RunWith(SettingsRobolectricTestRunner.class) public class WorkModeConditionControllerTest { + @Mock + private ConditionManager mConditionManager; private Context mContext; private WorkModeConditionController mController; @Before public void setUp() { + MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - mController = new WorkModeConditionController(mContext); + mController = new WorkModeConditionController(mContext, mConditionManager); } @Test From 639ad90313cc58ea05aea78b6e819f763cff2fd0 Mon Sep 17 00:00:00 2001 From: Doris Ling Date: Thu, 23 Aug 2018 13:25:06 -0700 Subject: [PATCH 11/12] Fix crash in clicking Default Home gear icon. - change the way that default home query the resolve activity to only check for LAUNCHER activity. Change-Id: Ib53154afe7951f4e2c7c3ab147be4c691113e0e7 Fixes: 112249115 Test: make RunSettingsRoboTests --- .../DefaultHomePreferenceController.java | 2 +- .../DefaultHomePreferenceControllerTest.java | 49 ++++++++++--------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java b/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java index 872f5a34d80..9fbde3e25b8 100644 --- a/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java +++ b/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceController.java @@ -105,7 +105,7 @@ public class DefaultHomePreferenceController extends DefaultAppPreferenceControl Intent intent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES) .setPackage(packageName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - return mPackageManager.queryIntentActivities(intent, 0).size() == 1 ? intent : null; + return intent.resolveActivity(mPackageManager) != null ? intent : null; } public static boolean hasHomePreference(String pkg, Context context) { diff --git a/tests/robotests/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceControllerTest.java index ce9fe2a837e..1f623211391 100644 --- a/tests/robotests/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/defaultapps/DefaultHomePreferenceControllerTest.java @@ -17,8 +17,8 @@ package com.android.settings.applications.defaultapps; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyList; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -29,6 +29,8 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.UserManager; @@ -45,14 +47,14 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; -import java.util.Arrays; -import java.util.Collections; - import androidx.preference.Preference; @RunWith(SettingsRobolectricTestRunner.class) public class DefaultHomePreferenceControllerTest { + private static final String TEST_PACKAGE = "test.pkg"; + private static final String TEST_CLASS = "class"; + @Mock private UserManager mUserManager; @Mock(answer = Answers.RETURNS_DEEP_STUBS) @@ -107,14 +109,14 @@ public class DefaultHomePreferenceControllerTest { @Test public void testIsHomeDefault_noDefaultSet_shouldReturnTrue() { when(mPackageManager.getHomeActivities(anyList())).thenReturn(null); - assertThat(DefaultHomePreferenceController.isHomeDefault("test.pkg", mPackageManager)) + assertThat(DefaultHomePreferenceController.isHomeDefault(TEST_PACKAGE, mPackageManager)) .isTrue(); } @Test public void testIsHomeDefault_defaultSetToPkg_shouldReturnTrue() { - final String pkgName = "test.pkg"; - final ComponentName defaultHome = new ComponentName(pkgName, "class"); + final String pkgName = TEST_PACKAGE; + final ComponentName defaultHome = new ComponentName(pkgName, TEST_CLASS); when(mPackageManager.getHomeActivities(anyList())).thenReturn(defaultHome); @@ -124,8 +126,8 @@ public class DefaultHomePreferenceControllerTest { @Test public void testIsHomeDefault_defaultSetToOtherPkg_shouldReturnFalse() { - final String pkgName = "test.pkg"; - final ComponentName defaultHome = new ComponentName("not" + pkgName, "class"); + final String pkgName = TEST_PACKAGE; + final ComponentName defaultHome = new ComponentName("not" + pkgName, TEST_CLASS); when(mPackageManager.getHomeActivities(anyList())).thenReturn(defaultHome); @@ -136,29 +138,28 @@ public class DefaultHomePreferenceControllerTest { @Test public void testGetSettingIntent_homeHasNoSetting_shouldNotReturnSettingIntent() { when(mPackageManager.getHomeActivities(anyList())) - .thenReturn(new ComponentName("test.pkg", "class")); + .thenReturn(new ComponentName(TEST_PACKAGE, TEST_CLASS)); + when(mPackageManager.resolveActivity(any(Intent.class), anyInt())) + .thenReturn(null); + assertThat(mController.getSettingIntent(mController.getDefaultAppInfo())).isNull(); } @Test public void testGetSettingIntent_homeHasOneSetting_shouldReturnSettingIntent() { when(mPackageManager.getHomeActivities(anyList())) - .thenReturn(new ComponentName("test.pkg", "class")); - when(mPackageManager.queryIntentActivities(any(), eq(0))) - .thenReturn(Collections.singletonList(mock(ResolveInfo.class))); + .thenReturn(new ComponentName(TEST_PACKAGE, TEST_CLASS)); + final ResolveInfo info = mock(ResolveInfo.class); + info.activityInfo = mock(ActivityInfo.class); + info.activityInfo.name = TEST_CLASS; + info.activityInfo.applicationInfo = mock(ApplicationInfo.class); + info.activityInfo.applicationInfo.packageName = TEST_PACKAGE; + when(mPackageManager.resolveActivity(any(Intent.class), anyInt())) + .thenReturn(info); Intent intent = mController.getSettingIntent(mController.getDefaultAppInfo()); assertThat(intent).isNotNull(); - assertThat(intent.getPackage()).isEqualTo("test.pkg"); - } - - @Test - public void testGetSettingIntent_homeHasMultipleSettings_shouldNotReturnSettingIntent() { - when(mPackageManager.getHomeActivities(anyList())) - .thenReturn(new ComponentName("test.pkg", "class")); - when(mPackageManager.queryIntentActivities(any(), eq(0))) - .thenReturn(Arrays.asList(mock(ResolveInfo.class), mock(ResolveInfo.class))); - assertThat(mController.getSettingIntent(mController.getDefaultAppInfo())).isNull(); + assertThat(intent.getPackage()).isEqualTo(TEST_PACKAGE); } @Test From 6cb08a3ce468add4ea15b7398ed28bf22a71f66c Mon Sep 17 00:00:00 2001 From: Emily Chuang Date: Wed, 25 Jul 2018 20:35:06 +0800 Subject: [PATCH 12/12] Create the main architecture for Settings Dynamic Homepage Create an architecture for the homepage to enable card adding and displaying. Bug: 111676964 Test: robotests Change-Id: I06efe3d16585060a8e99313586ae29bbde7fe3e8 --- res/layout/dashboard.xml | 3 +- .../settings/homepage/CardContentLoader.java | 62 ++++ .../homepage/ControllerRendererPool.java | 99 ++++++ .../settings/homepage/HomepageAdapter.java | 96 ++++++ .../settings/homepage/HomepageCard.java | 296 ++++++++++++++++++ .../homepage/HomepageCardController.java | 45 +++ .../homepage/HomepageCardLookupTable.java | 73 +++++ .../homepage/HomepageCardRenderer.java | 46 +++ .../homepage/HomepageCardUpdateListener.java | 31 ++ .../settings/homepage/HomepageFragment.java | 12 + .../settings/homepage/HomepageManager.java | 135 ++++++++ 11 files changed, 896 insertions(+), 2 deletions(-) create mode 100644 src/com/android/settings/homepage/CardContentLoader.java create mode 100644 src/com/android/settings/homepage/ControllerRendererPool.java create mode 100644 src/com/android/settings/homepage/HomepageAdapter.java create mode 100644 src/com/android/settings/homepage/HomepageCard.java create mode 100644 src/com/android/settings/homepage/HomepageCardController.java create mode 100644 src/com/android/settings/homepage/HomepageCardLookupTable.java create mode 100644 src/com/android/settings/homepage/HomepageCardRenderer.java create mode 100644 src/com/android/settings/homepage/HomepageCardUpdateListener.java create mode 100644 src/com/android/settings/homepage/HomepageManager.java diff --git a/res/layout/dashboard.xml b/res/layout/dashboard.xml index ccb50ae10ed..8031028d8c6 100644 --- a/res/layout/dashboard.xml +++ b/res/layout/dashboard.xml @@ -26,5 +26,4 @@ android:paddingEnd="@dimen/dashboard_padding_end" android:paddingTop="@dimen/dashboard_padding_top" android:paddingBottom="@dimen/dashboard_padding_bottom" - android:scrollbars="vertical"/> - + android:scrollbars="vertical"/> \ No newline at end of file diff --git a/src/com/android/settings/homepage/CardContentLoader.java b/src/com/android/settings/homepage/CardContentLoader.java new file mode 100644 index 00000000000..4e1e33ef891 --- /dev/null +++ b/src/com/android/settings/homepage/CardContentLoader.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2018 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; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.util.List; + +//TODO(b/112521307): Implement this to make it work with the card database. +public class CardContentLoader { + + private static final String TAG = "CardContentLoader"; + + private CardContentLoaderListener mListener; + + public interface CardContentLoaderListener { + void onFinishCardLoading(List homepageCards); + } + + public CardContentLoader() { + } + + void setListener(CardContentLoaderListener listener) { + mListener = listener; + } + + private static class CardLoader extends AsyncLoaderCompat> { + + public CardLoader(Context context) { + super(context); + } + + @Override + protected void onDiscardResult(List result) { + + } + + @Nullable + @Override + public List loadInBackground() { + return null; + } + } +} diff --git a/src/com/android/settings/homepage/ControllerRendererPool.java b/src/com/android/settings/homepage/ControllerRendererPool.java new file mode 100644 index 00000000000..b2ac9ecde9f --- /dev/null +++ b/src/com/android/settings/homepage/ControllerRendererPool.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 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; + +import android.content.Context; +import android.util.Log; + +import androidx.collection.ArraySet; + +import java.util.Set; + +/** + * This is a fragment scoped singleton holding a set of {@link HomepageCardController} and + * {@link HomepageCardRenderer}. + */ +public class ControllerRendererPool { + + private static final String TAG = "ControllerRendererPool"; + + private final Set mControllers; + private final Set mRenderers; + + public ControllerRendererPool() { + mControllers = new ArraySet<>(); + mRenderers = new ArraySet<>(); + } + + public T getController(Context context, + @HomepageCard.CardType int cardType) { + final Class clz = + HomepageCardLookupTable.getCardControllerClass(cardType); + for (HomepageCardController controller : mControllers) { + if (controller.getClass() == clz) { + Log.d(TAG, "Controller is already there."); + return (T) controller; + } + } + + final HomepageCardController controller = createCardController(context, clz); + if (controller != null) { + mControllers.add(controller); + } + return (T) controller; + } + + public Set getControllers() { + return mControllers; + } + + public HomepageCardRenderer getRenderer(Context context, @HomepageCard.CardType int cardType) { + final Class clz = + HomepageCardLookupTable.getCardRendererClasses(cardType); + for (HomepageCardRenderer renderer : mRenderers) { + if (renderer.getClass() == clz) { + Log.d(TAG, "Renderer is already there."); + return renderer; + } + } + + final HomepageCardRenderer renderer = createCardRenderer(context, clz); + if (renderer != null) { + mRenderers.add(renderer); + } + return renderer; + } + + private HomepageCardController createCardController(Context context, + Class clz) { + /* + if (ConditionHomepageCardController.class == clz) { + return new ConditionHomepageCardController(context); + } + */ + return null; + } + + private HomepageCardRenderer createCardRenderer(Context context, Class clz) { + //if (ConditionHomepageCardRenderer.class == clz) { + // return new ConditionHomepageCardRenderer(context, this /*controllerRendererPool*/); + //} + + return null; + } + +} diff --git a/src/com/android/settings/homepage/HomepageAdapter.java b/src/com/android/settings/homepage/HomepageAdapter.java new file mode 100644 index 00000000000..b44288337d4 --- /dev/null +++ b/src/com/android/settings/homepage/HomepageAdapter.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2018 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; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +public class HomepageAdapter extends RecyclerView.Adapter implements + HomepageCardUpdateListener { + + private static final String TAG = "HomepageAdapter"; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + + private List mHomepageCards; + private RecyclerView mRecyclerView; + + public HomepageAdapter(Context context, HomepageManager manager) { + mContext = context; + mHomepageCards = new ArrayList<>(); + mControllerRendererPool = manager.getControllerRendererPool(); + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return mHomepageCards.get(position).hashCode(); + } + + @Override + public int getItemViewType(int position) { + return mHomepageCards.get(position).getCardType(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int cardType) { + final HomepageCardRenderer renderer = mControllerRendererPool.getRenderer(mContext, cardType); + final int viewType = renderer.getViewType(); + final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + + return renderer.createViewHolder(view); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + final int cardType = mHomepageCards.get(position).getCardType(); + final HomepageCardRenderer renderer = mControllerRendererPool.getRenderer(mContext, cardType); + + renderer.bindView(holder, mHomepageCards.get(position)); + } + + @Override + public int getItemCount() { + return mHomepageCards.size(); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + mRecyclerView = recyclerView; + } + + @Override + public void onHomepageCardUpdated(int cardType, List homepageCards) { + //TODO(b/112245748): Should implement a DiffCallback so we can use notifyItemChanged() + // instead. + if (homepageCards == null) { + mHomepageCards.clear(); + } else { + mHomepageCards = homepageCards; + } + notifyDataSetChanged(); + } +} diff --git a/src/com/android/settings/homepage/HomepageCard.java b/src/com/android/settings/homepage/HomepageCard.java new file mode 100644 index 00000000000..1719f57869a --- /dev/null +++ b/src/com/android/settings/homepage/HomepageCard.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2018 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; + +import android.annotation.IntDef; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.text.TextUtils; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Data class representing a {@link HomepageCard}. + */ +public class HomepageCard { + + /** + * Flags indicating the type of the HomepageCard. + */ + @IntDef({CardType.INVALID, CardType.SLICE, CardType.SUGGESTION, CardType.CONDITIONAL}) + @Retention(RetentionPolicy.SOURCE) + public @interface CardType { + int INVALID = -1; + int SLICE = 1; + int SUGGESTION = 2; + int CONDITIONAL = 3; + } + + private final String mName; + @CardType + private final int mCardType; + private final double mScore; + private final String mSliceUri; + private final int mCategory; + private final String mLocalizedToLocale; + private final String mPackageName; + private final String mAppVersion; + private final String mTitleResName; + private final String mTitleText; + private final String mSummaryResName; + private final String mSummaryText; + private final String mIconResName; + private final int mIconResId; + private final String mCardAction; + private final long mExpireTimeMS; + private final Drawable mDrawable; + private final boolean mSupportHalfWidth; + + String getName() { + return mName; + } + + int getCardType() { + return mCardType; + } + + double getScore() { + return mScore; + } + + String getTextSliceUri() { + return mSliceUri; + } + + Uri getSliceUri() { + return Uri.parse(mSliceUri); + } + + int getCategory() { + return mCategory; + } + + String getLocalizedToLocale() { + return mLocalizedToLocale; + } + + String getPackageName() { + return mPackageName; + } + + String getAppVersion() { + return mAppVersion; + } + + String getTitleResName() { + return mTitleResName; + } + + String getTitleText() { + return mTitleText; + } + + String getSummaryResName() { + return mSummaryResName; + } + + String getSummaryText() { + return mSummaryText; + } + + String getIconResName() { + return mIconResName; + } + + int getIconResId() { + return mIconResId; + } + + String getCardAction() { + return mCardAction; + } + + long getExpireTimeMS() { + return mExpireTimeMS; + } + + Drawable getDrawable() { + return mDrawable; + } + + boolean getSupportHalfWidth() { + return mSupportHalfWidth; + } + + HomepageCard(Builder builder) { + mName = builder.mName; + mCardType = builder.mCardType; + mScore = builder.mScore; + mSliceUri = builder.mSliceUri; + mCategory = builder.mCategory; + mLocalizedToLocale = builder.mLocalizedToLocale; + mPackageName = builder.mPackageName; + mAppVersion = builder.mAppVersion; + mTitleResName = builder.mTitleResName; + mTitleText = builder.mTitleText; + mSummaryResName = builder.mSummaryResName; + mSummaryText = builder.mSummaryText; + mIconResName = builder.mIconResName; + mIconResId = builder.mIconResId; + mCardAction = builder.mCardAction; + mExpireTimeMS = builder.mExpireTimeMS; + mDrawable = builder.mDrawable; + mSupportHalfWidth = builder.mSupportHalfWidth; + } + + @Override + public int hashCode() { + return mName.hashCode(); + } + + /** + * Note that {@link #mName} is treated as a primary key for this class and determines equality. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof HomepageCard)) { + return false; + } + final HomepageCard that = (HomepageCard) obj; + + return TextUtils.equals(mName, that.mName); + } + + static class Builder { + private String mName; + private int mCardType; + private double mScore; + private String mSliceUri; + private int mCategory; + private String mLocalizedToLocale; + private String mPackageName; + private String mAppVersion; + private String mTitleResName; + private String mTitleText; + private String mSummaryResName; + private String mSummaryText; + private String mIconResName; + private int mIconResId; + private String mCardAction; + private long mExpireTimeMS; + private Drawable mDrawable; + private boolean mSupportHalfWidth; + + public Builder setName(String name) { + mName = name; + return this; + } + + public Builder setCardType(int cardType) { + mCardType = cardType; + return this; + } + + public Builder setScore(double score) { + mScore = score; + return this; + } + + public Builder setSliceUri(String sliceUri) { + mSliceUri = sliceUri; + return this; + } + + public Builder setCategory(int category) { + mCategory = category; + return this; + } + + public Builder setLocalizedToLocale(String localizedToLocale) { + mLocalizedToLocale = localizedToLocale; + return this; + } + + public Builder setPackageName(String packageName) { + mPackageName = packageName; + return this; + } + + public Builder setAppVersion(String appVersion) { + mAppVersion = appVersion; + return this; + } + + public Builder setTitleResName(String titleResName) { + mTitleResName = titleResName; + return this; + } + + public Builder setTitleText(String titleText) { + mTitleText = titleText; + return this; + } + + public Builder setSummaryResName(String summaryResName) { + mSummaryResName = summaryResName; + return this; + } + + public Builder setSummaryText(String summaryText) { + mSummaryText = summaryText; + return this; + } + + public Builder setIconResName(String iconResName) { + mIconResName = iconResName; + return this; + } + + public Builder setIconResId(int iconResId) { + mIconResId = iconResId; + return this; + } + + public Builder setCardAction(String cardAction) { + mCardAction = cardAction; + return this; + } + + public Builder setExpireTimeMS(long expireTimeMS) { + mExpireTimeMS = expireTimeMS; + return this; + } + + public Builder setDrawable(Drawable drawable) { + mDrawable = drawable; + return this; + } + + public Builder setSupportHalfWidth(boolean supportHalfWidth) { + mSupportHalfWidth = supportHalfWidth; + return this; + } + + public HomepageCard build() { + return new HomepageCard(this); + } + } +} diff --git a/src/com/android/settings/homepage/HomepageCardController.java b/src/com/android/settings/homepage/HomepageCardController.java new file mode 100644 index 00000000000..c35c3c80c61 --- /dev/null +++ b/src/com/android/settings/homepage/HomepageCardController.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +//TODO(b/111821137): add test cases +/** + * Data controller for {@link HomepageCard}. + */ +public interface HomepageCardController { + + @HomepageCard.CardType + int getCardType(); + + /** + * When data is updated or changed, the new data should be passed to HomepageManager for list + * updating. + */ + void onDataUpdated(List cardList); + + void onPrimaryClick(HomepageCard card); + + void onActionClick(HomepageCard card); + + void setLifecycle(Lifecycle lifecycle); + + void setHomepageCardUpdateListener(HomepageCardUpdateListener listener); +} diff --git a/src/com/android/settings/homepage/HomepageCardLookupTable.java b/src/com/android/settings/homepage/HomepageCardLookupTable.java new file mode 100644 index 00000000000..9e941e9dd3f --- /dev/null +++ b/src/com/android/settings/homepage/HomepageCardLookupTable.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2018 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; + +import com.android.settings.homepage.HomepageCard.CardType; + +import java.util.Set; +import java.util.TreeSet; + +public class HomepageCardLookupTable { + + static class HomepageMapping implements Comparable { + @CardType + private final int mCardType; + private final Class mControllerClass; + private final Class mRendererClass; + + private HomepageMapping(@CardType int cardType, + Class controllerClass, + Class rendererClass) { + mCardType = cardType; + mControllerClass = controllerClass; + mRendererClass = rendererClass; + } + + @Override + public int compareTo(HomepageMapping other) { + return Integer.compare(this.mCardType, other.mCardType); + } + } + + private static final Set LOOKUP_TABLE = new TreeSet() { + { + //add(new HomepageMapping(CardType.CONDITIONAL, ConditionHomepageCardController.class, + // ConditionHomepageCardRenderer.class)); + } + }; + + public static Class getCardControllerClass( + @CardType int cardType) { + for (HomepageMapping mapping : LOOKUP_TABLE) { + if (mapping.mCardType == cardType) { + return mapping.mControllerClass; + } + } + return null; + } + + //TODO(b/112578070): Implement multi renderer cases. + public static Class getCardRendererClasses( + @CardType int cardType) { + for (HomepageMapping mapping : LOOKUP_TABLE) { + if (mapping.mCardType == cardType) { + return mapping.mRendererClass; + } + } + return null; + } +} diff --git a/src/com/android/settings/homepage/HomepageCardRenderer.java b/src/com/android/settings/homepage/HomepageCardRenderer.java new file mode 100644 index 00000000000..ffa54e36a69 --- /dev/null +++ b/src/com/android/settings/homepage/HomepageCardRenderer.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * UI renderer for {@link HomepageCard}. + */ +public interface HomepageCardRenderer { + + /** + * The layout type of the controller. + */ + int getViewType(); + + /** + * When {@link HomepageAdapter} calls {@link HomepageAdapter#onCreateViewHolder(ViewGroup, + * int)}, this method will be called to retrieve the corresponding + * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. + */ + RecyclerView.ViewHolder createViewHolder(View view); + + /** + * When {@link HomepageAdapter} calls {@link HomepageAdapter#onBindViewHolder(RecyclerView + * .ViewHolder, int)}, this method will be called to bind data to the + * {@link androidx.recyclerview.widget.RecyclerView.ViewHolder}. + */ + void bindView(RecyclerView.ViewHolder holder, HomepageCard card); +} \ No newline at end of file diff --git a/src/com/android/settings/homepage/HomepageCardUpdateListener.java b/src/com/android/settings/homepage/HomepageCardUpdateListener.java new file mode 100644 index 00000000000..a44ba2be6e3 --- /dev/null +++ b/src/com/android/settings/homepage/HomepageCardUpdateListener.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 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; + +import java.util.List; + +/** + * When {@link HomepageCardController} detects changes, it will notify the listeners registered. In + * our case, {@link HomepageManager} gets noticed. + * + * After the list of {@link HomepageCard} gets updated in{@link HomepageManager}, + * {@link HomepageManager} will notify the listeners registered, {@link HomepageAdapter} in this + * case. + */ +interface HomepageCardUpdateListener { + void onHomepageCardUpdated(int cardType, List updateList); +} \ No newline at end of file diff --git a/src/com/android/settings/homepage/HomepageFragment.java b/src/com/android/settings/homepage/HomepageFragment.java index dc6a91fe27d..402725ebc90 100644 --- a/src/com/android/settings/homepage/HomepageFragment.java +++ b/src/com/android/settings/homepage/HomepageFragment.java @@ -48,6 +48,7 @@ public class HomepageFragment extends InstrumentedFragment { private static final String SAVE_BOTTOM_FRAGMENT_LOADED = "bottom_fragment_loaded"; private RecyclerView mCardsContainer; + private HomepageAdapter mHomepageAdapter; private LinearLayoutManager mLayoutManager; private FloatingActionButton mSearchButton; @@ -55,6 +56,14 @@ public class HomepageFragment extends InstrumentedFragment { private View mBottomBar; private View mSearchBar; private boolean mBottomFragmentLoaded; + private HomepageManager mHomepageManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHomepageManager = new HomepageManager(getContext(), getSettingsLifecycle()); + mHomepageManager.startCardContentLoading(); + } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -65,6 +74,9 @@ public class HomepageFragment extends InstrumentedFragment { //TODO(b/111822407): May have to swap to GridLayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); mCardsContainer.setLayoutManager(mLayoutManager); + mHomepageAdapter = new HomepageAdapter(getContext(), mHomepageManager); + mCardsContainer.setAdapter(mHomepageAdapter); + mHomepageManager.setListener(mHomepageAdapter); return rootView; } diff --git a/src/com/android/settings/homepage/HomepageManager.java b/src/com/android/settings/homepage/HomepageManager.java new file mode 100644 index 00000000000..cbd5841d2cf --- /dev/null +++ b/src/com/android/settings/homepage/HomepageManager.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 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; + +import android.content.Context; +import android.widget.BaseAdapter; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a centralized manager of multiple {@link HomepageCardController}. + * + * {@link HomepageManager} first loads data from {@link CardContentLoader} and gets back a list of + * {@link HomepageCard}. All subclasses of {@link HomepageCardController} are loaded here, which + * will then trigger the {@link HomepageCardController} to load its data and listen to + * corresponding changes. When every single {@link HomepageCardController} updates its data, the + * data will be passed here, then going through some sorting mechanisms. The + * {@link HomepageCardController} will end up building a list of {@link HomepageCard} for {@link + * HomepageAdapter} and {@link BaseAdapter#notifyDataSetChanged()} will be called to get the page + * refreshed. + */ +public class HomepageManager implements CardContentLoader.CardContentLoaderListener, + HomepageCardUpdateListener { + + private static final String TAG = "HomepageManager"; + //The list for Settings Custom Card + @HomepageCard.CardType + private static final int[] SETTINGS_CARDS = {HomepageCard.CardType.CONDITIONAL}; + + private final Context mContext; + private final ControllerRendererPool mControllerRendererPool; + private final Lifecycle mLifecycle; + + private List mHomepageCards; + private HomepageCardUpdateListener mListener; + + + public HomepageManager(Context context, Lifecycle lifecycle) { + mContext = context; + mLifecycle = lifecycle; + mHomepageCards = new ArrayList<>(); + mControllerRendererPool = new ControllerRendererPool(); + } + + void startCardContentLoading() { + final CardContentLoader cardContentLoader = new CardContentLoader(); + cardContentLoader.setListener(this); + } + + private void loadCardControllers() { + if (mHomepageCards != null) { + for (HomepageCard card : mHomepageCards) { + setupController(card.getCardType()); + } + } + + //for data provided by Settings + for (int cardType : SETTINGS_CARDS) { + setupController(cardType); + } + } + + private void setupController(int cardType) { + final HomepageCardController controller = mControllerRendererPool.getController(mContext, + cardType); + if (controller != null) { + controller.setHomepageCardUpdateListener(this); + controller.setLifecycle(mLifecycle); + } + } + + //TODO(b/111822376): implement sorting mechanism. + private void sortCards() { + //take mHomepageCards as the source and do the ranking based on the rule. + } + + @Override + public void onHomepageCardUpdated(int cardType, List updateList) { + //TODO(b/112245748): Should implement a DiffCallback. + //Keep the old list for comparison. + final List prevCards = mHomepageCards; + + //Remove the existing data that matches the certain cardType so as to insert the new data. + for (int i = mHomepageCards.size() - 1; i >= 0; i--) { + if (mHomepageCards.get(i).getCardType() == cardType) { + mHomepageCards.remove(i); + } + } + + //Append the new data + mHomepageCards.addAll(updateList); + + sortCards(); + + if (mListener != null) { + mListener.onHomepageCardUpdated(HomepageCard.CardType.INVALID, mHomepageCards); + } + } + + @Override + public void onFinishCardLoading(List homepageCards) { + mHomepageCards = homepageCards; + + //Force card sorting here in case CardControllers of custom view have nothing to update + // for the first launch. + sortCards(); + + loadCardControllers(); + } + + void setListener(HomepageCardUpdateListener listener) { + mListener = listener; + } + + public ControllerRendererPool getControllerRendererPool() { + return mControllerRendererPool; + } +}