From 43ded696dd1c35e5a9b0b20c6584d519e0284c2b Mon Sep 17 00:00:00 2001 From: Neil Fuller Date: Tue, 16 May 2023 17:29:14 +0100 Subject: [PATCH] UI changes to support time feedback The feature is behind a release flag. It is also behind an experiment flag so it can be trialed with Googlers before general release even after being enabled in a release. The feedback button only shows up if there is an intent URI configured, which should be handled via an overlay. The design means that the intent is potentially dependent on the manufacturer (good!), though I expect we will suggest a standard one for GMS devices so we get feedback from a variety of devices with different form factors / capabilities. In this default, GMS core (Google Play Services) will handle the intent and take the user through a feedback UI flow. Testing: To enable the button you need to build with one of release variants that supports dynamic flags, e.g. trunk_food. Then release flag: $ adb shell device_config put location com.android.settings.flags.datetime_feedback true It still won't work without the experiment flag: $ adb shell device_config put settings_ui time_help_and_feedback_feature_supported true Finally, the settings entry will launch an intent when pressed which has to have a receiver. The receiver will be in GMS core but will be subject to its own review / launch process. Until then, this feature will remain quiet, biding its time. Bug: 283239837 Test: Manual (see above) Test: atest SettingsRoboTests:com.android.settings.datetime Change-Id: I68798798fc0a47ae4c6755174ce509fbaee24142 --- ...ettings_datetime_flag_declarations.aconfig | 11 +++ res/values/config.xml | 3 + res/values/strings.xml | 9 ++ res/xml/date_time_prefs.xml | 15 +++ .../settings/datetime/DateTimeSettings.java | 6 ++ .../datetime/TimeFeedbackLaunchUtils.java | 52 ++++++++++ ...eFeedbackPreferenceCategoryController.java | 67 +++++++++++++ .../TimeFeedbackPreferenceController.java | 95 ++++++++++++++++++ ...dbackPreferenceCategoryControllerTest.java | 97 +++++++++++++++++++ .../TimeFeedbackPreferenceControllerTest.java | 90 +++++++++++++++++ 10 files changed, 445 insertions(+) create mode 100644 aconfig/settings_datetime_flag_declarations.aconfig create mode 100644 src/com/android/settings/datetime/TimeFeedbackLaunchUtils.java create mode 100644 src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryController.java create mode 100644 src/com/android/settings/datetime/TimeFeedbackPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceControllerTest.java diff --git a/aconfig/settings_datetime_flag_declarations.aconfig b/aconfig/settings_datetime_flag_declarations.aconfig new file mode 100644 index 00000000000..3d9d8b317a5 --- /dev/null +++ b/aconfig/settings_datetime_flag_declarations.aconfig @@ -0,0 +1,11 @@ +package: "com.android.settings.flags" +container: "system" + +flag { + name: "datetime_feedback" + # "location" is used by the Android System Time team for feature flags. + namespace: "location" + description: "Enable the time feedback feature, a button to launch feedback in Date & Time Settings" + bug: "283239837" +} + diff --git a/res/values/config.xml b/res/values/config.xml index 9e91dcc3c54..a52dafcc0f0 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -812,4 +812,7 @@ + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index d5fd6214b1e..5f0f762b08a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -611,6 +611,15 @@ Select by UTC offset + + Feedback + + feedback, bug, time, zone, timezone + + Send feedback about time + + feedback, bug, time, zone, timezone + diff --git a/res/xml/date_time_prefs.xml b/res/xml/date_time_prefs.xml index 32684666b74..3fb4a065d7c 100644 --- a/res/xml/date_time_prefs.xml +++ b/res/xml/date_time_prefs.xml @@ -73,6 +73,21 @@ + + + + + + + mChildControllers = new ArrayList<>(); + + public TimeFeedbackPreferenceCategoryController( + Context context, String preferenceKey) { + super(context, preferenceKey); + } + + /** + * Adds a controller whose own availability can determine the category's availability status. + */ + void addChildController(@NonNull AbstractPreferenceController childController) { + mChildControllers.add(Objects.requireNonNull(childController)); + } + + @Override + public int getAvailabilityStatus() { + // Firstly, hide the category if it is not enabled by flags. + if (!isTimeFeedbackFeatureEnabled()) { + return UNSUPPORTED_ON_DEVICE; + } + + // Secondly, only show the category if there's one or more controllers available within it. + for (AbstractPreferenceController childController : mChildControllers) { + if (childController.isAvailable()) { + return AVAILABLE; + } + } + return UNSUPPORTED_ON_DEVICE; + } + + protected boolean isTimeFeedbackFeatureEnabled() { + return TimeFeedbackLaunchUtils.isFeedbackFeatureSupported(); + } +} diff --git a/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java b/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java new file mode 100644 index 00000000000..5abe4af33cb --- /dev/null +++ b/src/com/android/settings/datetime/TimeFeedbackPreferenceController.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 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.datetime; + +import static android.content.Intent.URI_INTENT_SCHEME; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.text.TextUtils; + +import androidx.preference.Preference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.PreferenceControllerMixin; + +import java.net.URISyntaxException; + +/** + * A controller for the Settings button that launches "time feedback". The intent launches is + * configured with an Intent URI. + */ +public class TimeFeedbackPreferenceController + extends BasePreferenceController + implements PreferenceControllerMixin { + + private final String mIntentUri; + private final int mAvailabilityStatus; + + public TimeFeedbackPreferenceController(Context context, String preferenceKey) { + this(context, preferenceKey, context.getResources().getString( + R.string.config_time_feedback_intent_uri)); + } + + @VisibleForTesting + TimeFeedbackPreferenceController(Context context, String preferenceKey, String intentUri) { + super(context, preferenceKey); + mIntentUri = intentUri; + mAvailabilityStatus = TextUtils.isEmpty(mIntentUri) ? UNSUPPORTED_ON_DEVICE : AVAILABLE; + } + + /** + * Registers this controller with a category controller so that the category can be optionally + * displayed, i.e. if all the child controllers are not available, the category heading won't be + * available. + */ + public void registerWithOptionalCategoryController( + TimeFeedbackPreferenceCategoryController categoryController) { + categoryController.addChildController(this); + } + + @Override + public int getAvailabilityStatus() { + if (!TimeFeedbackLaunchUtils.isFeedbackFeatureSupported()) { + return UNSUPPORTED_ON_DEVICE; + } + return mAvailabilityStatus; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (!TextUtils.equals(preference.getKey(), getPreferenceKey())) { + return super.handlePreferenceTreeClick(preference); + } + + // Don't allow a monkey user to launch feedback + if (ActivityManager.isUserAMonkey()) { + return true; + } + + try { + Intent intent = Intent.parseUri(mIntentUri, URI_INTENT_SCHEME); + mContext.startActivity(intent); + return true; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Bad intent configuration: " + mIntentUri, e); + } + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java new file mode 100644 index 00000000000..1747f170bf4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceCategoryControllerTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 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.datetime; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.content.Context; + +import com.android.settingslib.core.AbstractPreferenceController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class TimeFeedbackPreferenceCategoryControllerTest { + + private TestTimeFeedbackPreferenceCategoryController mController; + @Mock private AbstractPreferenceController mChildController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + Context context = RuntimeEnvironment.getApplication(); + + mController = new TestTimeFeedbackPreferenceCategoryController(context, "test_key"); + mController.addChildController(mChildController); + } + + @Test + public void getAvailabilityStatus_featureEnabledPrimary() { + mController.setTimeFeedbackFeatureEnabled(false); + + when(mChildController.isAvailable()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getAvailabilityStatus_childControllerSecondary() { + mController.setTimeFeedbackFeatureEnabled(true); + + when(mChildController.isAvailable()).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + + when(mChildController.isAvailable()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + /** + * Extend class under test to change {@link #isTimeFeedbackFeatureEnabled} to not call + * {@link TimeFeedbackLaunchUtils} because that's non-trivial to fake. + */ + private static class TestTimeFeedbackPreferenceCategoryController + extends TimeFeedbackPreferenceCategoryController { + + private boolean mTimeFeedbackFeatureEnabled; + + TestTimeFeedbackPreferenceCategoryController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + void setTimeFeedbackFeatureEnabled(boolean value) { + this.mTimeFeedbackFeatureEnabled = value; + } + + @Override + protected boolean isTimeFeedbackFeatureEnabled() { + return mTimeFeedbackFeatureEnabled; + } + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceControllerTest.java new file mode 100644 index 00000000000..f60e8319cb7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/TimeFeedbackPreferenceControllerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 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.datetime; + +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; + +import androidx.preference.Preference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class TimeFeedbackPreferenceControllerTest { + + private Context mContext; + + @Before + public void setUp() { + mContext = spy(Robolectric.setupActivity(Activity.class)); + } + + @Test + public void emptyIntentUri_controllerNotAvailable() { + String emptyIntentUri = ""; + TimeFeedbackPreferenceController controller = + new TimeFeedbackPreferenceController(mContext, "test_key", emptyIntentUri); + assertThat(controller.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void clickPreference() { + Preference preference = new Preference(mContext); + + String intentUri = + "intent:#Intent;" + + "action=com.android.settings.test.LAUNCH_USER_FEEDBACK;" + + "package=com.android.settings.test.target;" + + "end"; + TimeFeedbackPreferenceController controller = + new TimeFeedbackPreferenceController(mContext, "test_key", intentUri); + + // Click a preference that's not controlled by this controller. + preference.setKey("fake_key"); + assertThat(controller.handlePreferenceTreeClick(preference)).isFalse(); + + // Check for startActivity() call. + verify(mContext, never()).startActivity(any()); + + // Click a preference controlled by this controller. + preference.setKey(controller.getPreferenceKey()); + assertThat(controller.handlePreferenceTreeClick(preference)).isTrue(); + + // Check for startActivity() call. + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).startActivity(intentCaptor.capture()); + Intent actualIntent = intentCaptor.getValue(); + assertThat(actualIntent.getAction()).isEqualTo( + "com.android.settings.test.LAUNCH_USER_FEEDBACK"); + assertThat(actualIntent.getPackage()).isEqualTo("com.android.settings.test.target"); + } +}