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
This commit is contained in:
Neil Fuller
2023-05-16 17:29:14 +01:00
parent 958d17bb69
commit 43ded696dd
10 changed files with 445 additions and 0 deletions

View File

@@ -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"
}

View File

@@ -812,4 +812,7 @@
<!-- Array of carrier id that uses reusable activation code--> <!-- Array of carrier id that uses reusable activation code-->
<integer-array name="config_carrier_use_rac" translatable="false"> <integer-array name="config_carrier_use_rac" translatable="false">
</integer-array> </integer-array>
<!-- The Activity intent to trigger to launch time-related feedback. -->
<string name="config_time_feedback_intent_uri" translatable="false" />
</resources> </resources>

View File

@@ -611,6 +611,15 @@
<!-- The menu item to switch to selecting a time zone with a fixed offset (such as UTC or GMT+0200) [CHAR LIMIT=30] --> <!-- The menu item to switch to selecting a time zone with a fixed offset (such as UTC or GMT+0200) [CHAR LIMIT=30] -->
<string name="zone_menu_by_offset">Select by UTC offset</string> <string name="zone_menu_by_offset">Select by UTC offset</string>
<!-- The settings category title containing the feedback button [CHAR LIMIT=30] -->
<string name="time_feedback_category_title">Feedback</string>
<!-- Search keywords for the feedback category / section in Date & Time settings. [CHAR_LIMIT=NONE] -->
<string name="keywords_time_feedback_category">feedback, bug, time, zone, timezone</string>
<!-- The menu item to start the feedback process [CHAR LIMIT=30] -->
<string name="time_feedback_title">Send feedback about time</string>
<!-- Search keywords for the feedback option in Date & Time settings. [CHAR_LIMIT=NONE] -->
<string name="keywords_time_feedback">feedback, bug, time, zone, timezone</string>
<!-- Security Settings --><skip /> <!-- Security Settings --><skip />
<!-- Security settings screen, setting option name to change screen timeout --> <!-- Security settings screen, setting option name to change screen timeout -->

View File

@@ -73,6 +73,21 @@
</PreferenceCategory> </PreferenceCategory>
<!-- An optional preference category for feedback. Only displayed up if enabled via flags and config. -->
<PreferenceCategory
android:key="time_feedback_preference_category"
android:title="@string/time_feedback_category_title"
settings:keywords="@string/keywords_time_feedback_category"
settings:controller="com.android.settings.datetime.TimeFeedbackPreferenceCategoryController">
<Preference
android:key="time_feedback"
android:title="@string/time_feedback_title"
settings:keywords="@string/keywords_time_feedback"
settings:controller="com.android.settings.datetime.TimeFeedbackPreferenceController" />
</PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="time_format_preference_category" android:key="time_format_preference_category"
android:title="@string/time_format_category_title" android:title="@string/time_format_category_title"

View File

@@ -68,6 +68,12 @@ public class DateTimeSettings extends DashboardFragment implements
.setTimeAndDateCallback(this) .setTimeAndDateCallback(this)
.setFromSUW(isFromSUW); .setFromSUW(isFromSUW);
// All the elements in the category are optional, so we must ensure the category is only
// available if any of the elements are available.
TimeFeedbackPreferenceCategoryController helpAndFeedbackCategoryController =
use(TimeFeedbackPreferenceCategoryController.class);
use(TimeFeedbackPreferenceController.class)
.registerWithOptionalCategoryController(helpAndFeedbackCategoryController);
} }
@Override @Override

View File

@@ -0,0 +1,52 @@
/*
* 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.provider.DeviceConfig.NAMESPACE_SETTINGS_UI;
import android.provider.DeviceConfig;
import com.android.settings.flags.Flags;
/** A class to avoid duplication of launch-control logic for "time feedback" support. */
final class TimeFeedbackLaunchUtils {
/**
* A {@link DeviceConfig} flag that influences whether the settings entries related to help and
* feedback are supported on this device / for this user.
*/
public static final String KEY_HELP_AND_FEEDBACK_FEATURE_SUPPORTED =
"time_help_and_feedback_feature_supported";
private TimeFeedbackLaunchUtils() {}
static boolean isFeedbackFeatureSupported() {
// Support is determined according to:
// 1) A build-time flag to determine release feature availability.
// 2) A runtime / server-side flag to determine which devices / who gets to see the feature.
// This is launch control for limiting the feedback to droidfooding.
return isFeatureSupportedThisRelease() && isFeatureSupportedOnThisDevice();
}
private static boolean isFeatureSupportedThisRelease() {
return Flags.datetimeFeedback();
}
private static boolean isFeatureSupportedOnThisDevice() {
boolean defaultIsSupported = false;
return DeviceConfig.getBoolean(
NAMESPACE_SETTINGS_UI, KEY_HELP_AND_FEEDBACK_FEATURE_SUPPORTED, defaultIsSupported);
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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 android.content.Context;
import androidx.annotation.NonNull;
import com.android.settings.core.BasePreferenceController;
import com.android.settingslib.core.AbstractPreferenceController;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* A controller for the Settings category for "time feedback".
*/
public class TimeFeedbackPreferenceCategoryController extends BasePreferenceController {
private final List<AbstractPreferenceController> 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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<Intent> 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");
}
}