diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7d47fefc120..bcda6853cb6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -115,6 +115,7 @@
+
-
-
-
-
-
-
-
-
-
-
-
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7f7c218b020..b839b6d4a8b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3542,8 +3542,6 @@
^1 ^2
-
- Used of %1$s
%1$s is mounted
@@ -13543,6 +13541,9 @@
+
+ All Services
+
Show clipboard access
diff --git a/res/xml/app_info_settings.xml b/res/xml/app_info_settings.xml
index 562c7d17192..bb5fbae7564 100644
--- a/res/xml/app_info_settings.xml
+++ b/res/xml/app_info_settings.xml
@@ -41,6 +41,12 @@
android:title="@string/app_settings_link"
settings:controller="com.android.settings.applications.appinfo.AppSettingPreferenceController" />
+
+
mHandler.post(r), mTetheringEventCallback);
mMassStorageActive = Environment.MEDIA_SHARED.equals(Environment.getExternalStorageState());
registerReceiver();
mEthernetListener = new EthernetListener();
if (mEm != null)
- mEm.addListener(mEthernetListener);
+ mEm.addListener(mEthernetListener, r -> mHandler.post(r));
updateUsbState();
updateBluetoothAndEthernetState();
diff --git a/src/com/android/settings/applications/appinfo/AppAllServicesPreferenceController.java b/src/com/android/settings/applications/appinfo/AppAllServicesPreferenceController.java
new file mode 100644
index 00000000000..9444e722217
--- /dev/null
+++ b/src/com/android/settings/applications/appinfo/AppAllServicesPreferenceController.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2022 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.appinfo;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.location.LocationManager;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceScreen;
+
+import java.util.Objects;
+
+/**
+ * Preference Controller for the "All Services" preference in the "App Info" page.
+ */
+public class AppAllServicesPreferenceController extends AppInfoPreferenceControllerBase {
+
+ private static final String TAG = "AllServicesPrefControl";
+ private static final String SUMMARY_METADATA_KEY = "app_features_preference_summary";
+
+ private final PackageManager mPackageManager;
+
+ private String mPackageName;
+
+ private boolean mCanPackageHandleAllServicesIntent;
+ private boolean mIsLocationProvider;
+
+
+ public AppAllServicesPreferenceController(Context context,
+ String preferenceKey) {
+ super(context, preferenceKey);
+ mPackageManager = context.getPackageManager();
+
+ // Set to false till we can confirm that the package can handle the intent.
+ mCanPackageHandleAllServicesIntent = false;
+ // Set to false till we can confirm that the package is a location provider.
+ mIsLocationProvider = false;
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ super.displayPreference(screen);
+ CharSequence summary = getStorageSummary();
+ if (summary != null) {
+ mPreference.setSummary(summary);
+ }
+ }
+
+ @VisibleForTesting
+ @Nullable
+ CharSequence getStorageSummary() {
+ ResolveInfo resolveInfo = getResolveInfo(PackageManager.GET_META_DATA);
+ if (resolveInfo == null) {
+ Log.d(TAG, "mResolveInfo is null.");
+ return null;
+ }
+ final Bundle metaData = resolveInfo.activityInfo.metaData;
+ if (metaData != null) {
+ try {
+ final Resources pkgRes = mPackageManager.getResourcesForActivity(
+ new ComponentName(mPackageName, resolveInfo.activityInfo.name));
+ return pkgRes.getString(metaData.getInt(SUMMARY_METADATA_KEY));
+ } catch (Resources.NotFoundException exception) {
+ Log.d(TAG, "Resource not found for summary string.");
+ } catch (PackageManager.NameNotFoundException exception) {
+ Log.d(TAG, "Name of resource not found for summary string.");
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ if (mCanPackageHandleAllServicesIntent && mIsLocationProvider) {
+ return AVAILABLE;
+ }
+ return CONDITIONALLY_UNAVAILABLE;
+ }
+
+ private boolean isLocationProvider() {
+ return Objects.requireNonNull(
+ mContext.getSystemService(LocationManager.class)).isProviderPackage(mPackageName);
+ }
+
+ private boolean canPackageHandleIntent() {
+ return getResolveInfo(0) != null;
+ }
+
+ @Override
+ public boolean handlePreferenceTreeClick(Preference preference) {
+ if (getPreferenceKey().equals(preference.getKey())) {
+ startAllServicesActivity();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Set the package name of the package for which the "All Services" activity needs to be shown.
+ *
+ * @param packageName Name of the package for which the services need to be shown.
+ */
+ public void setPackageName(String packageName) {
+ mPackageName = packageName;
+
+ //Once we have package name. Update conditions for availability.
+ updateAvailabilityConditions();
+ }
+
+ private void updateAvailabilityConditions() {
+ mCanPackageHandleAllServicesIntent = canPackageHandleIntent();
+ mIsLocationProvider = isLocationProvider();
+ }
+
+ private void startAllServicesActivity() {
+ final Intent featuresIntent = new Intent(Intent.ACTION_VIEW_APP_FEATURES);
+ // This won't be null since the preference is only shown for packages that can handle the
+ // intent.
+ ResolveInfo resolveInfo = getResolveInfo(0);
+ featuresIntent.setComponent(
+ new ComponentName(mPackageName, resolveInfo.activityInfo.name));
+
+ Activity activity = mParent.getActivity();
+ try {
+ if (activity != null) {
+ activity.startActivity(featuresIntent);
+ }
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "The app cannot handle android.intent.action.VIEW_APP_FEATURES");
+ }
+ }
+
+ @Nullable
+ private ResolveInfo getResolveInfo(int flags) {
+ if (mPackageName == null) {
+ return null;
+ }
+ final Intent featuresIntent = new Intent(Intent.ACTION_VIEW_APP_FEATURES);
+ featuresIntent.setPackage(mPackageName);
+
+ return mPackageManager.resolveActivity(featuresIntent, flags);
+ }
+}
diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
index 243dc564be8..6d22dd03021 100755
--- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
+++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
@@ -162,6 +162,8 @@ public class AppInfoDashboardFragment extends DashboardFragment
use(AppSettingPreferenceController.class)
.setPackageName(packageName)
.setParentFragment(this);
+ use(AppAllServicesPreferenceController.class).setParentFragment(this);
+ use(AppAllServicesPreferenceController.class).setPackageName(packageName);
use(AppStoragePreferenceController.class).setParentFragment(this);
use(AppVersionPreferenceController.class).setParentFragment(this);
use(InstantAppDomainsPreferenceController.class).setParentFragment(this);
diff --git a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
index 5315347afbc..f7dd85ab67a 100644
--- a/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
+++ b/src/com/android/settings/deviceinfo/PublicVolumeSettings.java
@@ -28,8 +28,6 @@ import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.os.storage.VolumeRecord;
import android.provider.DocumentsContract;
-import android.text.TextUtils;
-import android.text.format.Formatter;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@@ -40,8 +38,10 @@ import androidx.preference.PreferenceScreen;
import com.android.internal.util.Preconditions;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.deviceinfo.storage.StorageUtils;
import com.android.settings.deviceinfo.storage.StorageUtils.MountTask;
import com.android.settings.deviceinfo.storage.StorageUtils.UnmountTask;
+import com.android.settingslib.widget.UsageProgressBarPreference;
import java.io.File;
import java.util.Objects;
@@ -59,7 +59,7 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
private VolumeInfo mVolume;
private DiskInfo mDisk;
- private StorageSummaryPreference mSummary;
+ private UsageProgressBarPreference mSummary;
private Preference mMount;
private Preference mFormatPublic;
@@ -114,7 +114,7 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
addPreferencesFromResource(R.xml.device_info_storage_volume);
getPreferenceScreen().setOrderingAsAdded(true);
- mSummary = new StorageSummaryPreference(getPrefContext());
+ mSummary = new UsageProgressBarPreference(getPrefContext());
mMount = buildAction(R.string.storage_menu_mount);
mUnmount = new Button(getActivity());
@@ -162,12 +162,10 @@ public class PublicVolumeSettings extends SettingsPreferenceFragment {
final long freeBytes = file.getFreeSpace();
final long usedBytes = totalBytes - freeBytes;
- final Formatter.BytesResult result = Formatter.formatBytes(getResources(), usedBytes,
- 0);
- mSummary.setTitle(TextUtils.expandTemplate(getText(R.string.storage_size_large),
- result.value, result.units));
- mSummary.setSummary(getString(R.string.storage_volume_used,
- Formatter.formatFileSize(context, totalBytes)));
+ mSummary.setUsageSummary(StorageUtils.getStorageSummary(
+ context, R.string.storage_usage_summary, usedBytes));
+ mSummary.setTotalSummary(StorageUtils.getStorageSummary(
+ context, R.string.storage_total_summary, totalBytes));
mSummary.setPercent(usedBytes, totalBytes);
}
diff --git a/src/com/android/settings/deviceinfo/StorageSummaryPreference.java b/src/com/android/settings/deviceinfo/StorageSummaryPreference.java
deleted file mode 100644
index 72b67de1ddd..00000000000
--- a/src/com/android/settings/deviceinfo/StorageSummaryPreference.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2015 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.deviceinfo;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.util.MathUtils;
-import android.view.View;
-import android.widget.ProgressBar;
-import android.widget.TextView;
-
-import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
-
-import com.android.settings.R;
-
-public class StorageSummaryPreference extends Preference {
- private int mPercent = -1;
-
- public StorageSummaryPreference(Context context) {
- super(context);
-
- setLayoutResource(R.layout.storage_summary);
- setEnabled(false);
- }
-
- public void setPercent(long usedBytes, long totalBytes) {
- mPercent = MathUtils.constrain((int) ((usedBytes * 100) / totalBytes),
- (usedBytes > 0) ? 1 : 0, 100);
- }
-
- @Override
- public void onBindViewHolder(PreferenceViewHolder view) {
- final ProgressBar progress = (ProgressBar) view.findViewById(android.R.id.progress);
- if (mPercent != -1) {
- progress.setVisibility(View.VISIBLE);
- progress.setProgress(mPercent);
- progress.setScaleY(7f);
- } else {
- progress.setVisibility(View.GONE);
- }
-
- final TextView summary = (TextView) view.findViewById(android.R.id.summary);
- summary.setTextColor(Color.parseColor("#8a000000"));
-
- super.onBindViewHolder(view);
- }
-}
diff --git a/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java
index cfee6a435b7..766035307e9 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageUsageProgressBarPreferenceController.java
@@ -18,7 +18,6 @@ package com.android.settings.deviceinfo.storage;
import android.app.usage.StorageStatsManager;
import android.content.Context;
-import android.text.format.Formatter;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -113,16 +112,10 @@ public class StorageUsageProgressBarPreferenceController extends BasePreferenceC
return;
}
mIsUpdateStateFromSelectedStorageEntry = false;
- mUsageProgressBarPreference.setUsageSummary(
- getStorageSummary(R.string.storage_usage_summary, mUsedBytes));
- mUsageProgressBarPreference.setTotalSummary(
- getStorageSummary(R.string.storage_total_summary, mTotalBytes));
+ mUsageProgressBarPreference.setUsageSummary(StorageUtils.getStorageSummary(
+ mContext, R.string.storage_usage_summary, mUsedBytes));
+ mUsageProgressBarPreference.setTotalSummary(StorageUtils.getStorageSummary(
+ mContext, R.string.storage_total_summary, mTotalBytes));
mUsageProgressBarPreference.setPercent(mUsedBytes, mTotalBytes);
}
-
- private String getStorageSummary(int resId, long bytes) {
- final Formatter.BytesResult result = Formatter.formatBytes(mContext.getResources(),
- bytes, Formatter.FLAG_SHORTER);
- return mContext.getString(resId, result.value, result.units);
- }
}
diff --git a/src/com/android/settings/deviceinfo/storage/StorageUtils.java b/src/com/android/settings/deviceinfo/storage/StorageUtils.java
index 9b52fe803b1..f59c8ab50d1 100644
--- a/src/com/android/settings/deviceinfo/storage/StorageUtils.java
+++ b/src/com/android/settings/deviceinfo/storage/StorageUtils.java
@@ -199,7 +199,7 @@ public class StorageUtils {
}
}
- /* Shows information about system storage. */
+ /** Shows information about system storage. */
public static class SystemInfoFragment extends InstrumentedDialogFragment {
/** Shows the fragment. */
public static void show(Fragment parent) {
@@ -224,4 +224,11 @@ public class StorageUtils {
.create();
}
}
+
+ /** Gets a summary which has a byte size information. */
+ public static String getStorageSummary(Context context, int resId, long bytes) {
+ final Formatter.BytesResult result = Formatter.formatBytes(context.getResources(),
+ bytes, Formatter.FLAG_SHORTER);
+ return context.getString(resId, result.value, result.units);
+ }
}
diff --git a/src/com/android/settings/display/AutoRotateSwitchBarController.java b/src/com/android/settings/display/AutoRotateSwitchBarController.java
index e149ccb6c76..48dedfd1fa3 100644
--- a/src/com/android/settings/display/AutoRotateSwitchBarController.java
+++ b/src/com/android/settings/display/AutoRotateSwitchBarController.java
@@ -31,7 +31,7 @@ import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.widget.OnMainSwitchChangeListener;
/**
- * The switch controller for the location.
+ * The switch controller for auto-rotate.
*/
public class AutoRotateSwitchBarController implements OnMainSwitchChangeListener,
LifecycleObserver, OnStart, OnStop {
diff --git a/src/com/android/settings/display/SmartAutoRotateController.java b/src/com/android/settings/display/SmartAutoRotateController.java
index b88aa807f97..76a222aac75 100644
--- a/src/com/android/settings/display/SmartAutoRotateController.java
+++ b/src/com/android/settings/display/SmartAutoRotateController.java
@@ -63,7 +63,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
updateState(mPreference);
}
};
- private Preference mPreference;
+ protected Preference mPreference;
private RotationPolicy.RotationPolicyListener mRotationPolicyListener;
public SmartAutoRotateController(Context context, String preferenceKey) {
@@ -84,10 +84,14 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
if (!isRotationResolverServiceAvailable(mContext)) {
return UNSUPPORTED_ON_DEVICE;
}
- return !RotationPolicy.isRotationLocked(mContext) && hasSufficientPermission(mContext)
+ return !isRotationLocked() && hasSufficientPermission(mContext)
&& !isCameraLocked() && !isPowerSaveMode() ? AVAILABLE : DISABLED_DEPENDENT_SETTING;
}
+ protected boolean isRotationLocked() {
+ return RotationPolicy.isRotationLocked(mContext);
+ }
+
@Override
public void updateState(Preference preference) {
super.updateState(preference);
@@ -136,7 +140,7 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
@Override
public boolean isChecked() {
- return !RotationPolicy.isRotationLocked(mContext) && hasSufficientPermission(mContext)
+ return !isRotationLocked() && hasSufficientPermission(mContext)
&& !isCameraLocked() && !isPowerSaveMode() && Settings.Secure.getInt(
mContext.getContentResolver(),
CAMERA_AUTOROTATE, 0) == 1;
@@ -163,7 +167,10 @@ public class SmartAutoRotateController extends TogglePreferenceController implem
return R.string.menu_key_display;
}
- static boolean isRotationResolverServiceAvailable(Context context) {
+ /**
+ * Returns true if there is a {@link RotationResolverService} available
+ */
+ public static boolean isRotationResolverServiceAvailable(Context context) {
final PackageManager packageManager = context.getPackageManager();
final String resolvePackage = packageManager.getRotationResolverPackageName();
if (TextUtils.isEmpty(resolvePackage)) {
diff --git a/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java b/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java
index b083aefee0b..8bef7086817 100644
--- a/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java
+++ b/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragment.java
@@ -27,6 +27,7 @@ import android.provider.SearchIndexableResource;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.dashboard.DashboardFragment;
@@ -43,16 +44,17 @@ import java.util.List;
/**
* The fragment for on-screen keyboard settings which used to display user installed IMEs.
- *
- * TODO(b/207452897): Add test for AvailableVirtualKeyboardFragment
*/
@SearchIndexable
-public final class AvailableVirtualKeyboardFragment extends DashboardFragment
+public class AvailableVirtualKeyboardFragment extends DashboardFragment
implements InputMethodPreference.OnSavePreferenceListener {
private static final String TAG = "AvailableVirtualKeyboardFragment";
- private final ArrayList mInputMethodPreferenceList = new ArrayList<>();
- private InputMethodSettingValuesWrapper mInputMethodSettingValues;
+ @VisibleForTesting
+ final ArrayList mInputMethodPreferenceList = new ArrayList<>();
+
+ @VisibleForTesting
+ InputMethodSettingValuesWrapper mInputMethodSettingValues;
private Context mUserAwareContext;
private int mUserId;
@@ -118,7 +120,8 @@ public final class AvailableVirtualKeyboardFragment extends DashboardFragment
return SettingsEnums.ENABLE_VIRTUAL_KEYBOARDS;
}
- private void updateInputMethodPreferenceViews() {
+ @VisibleForTesting
+ void updateInputMethodPreferenceViews() {
mInputMethodSettingValues.refreshAllInputMethodAndSubtypes();
// Clear existing "InputMethodPreference"s
mInputMethodPreferenceList.clear();
diff --git a/src/com/android/settings/network/EthernetTetherPreferenceController.java b/src/com/android/settings/network/EthernetTetherPreferenceController.java
index 06cd6a7feed..5b2cab752be 100644
--- a/src/com/android/settings/network/EthernetTetherPreferenceController.java
+++ b/src/com/android/settings/network/EthernetTetherPreferenceController.java
@@ -48,13 +48,12 @@ public final class EthernetTetherPreferenceController extends TetherBasePreferen
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onStart() {
- mEthernetListener = new EthernetManager.Listener() {
- @Override
- public void onAvailabilityChanged(String iface, boolean isAvailable) {
- new Handler(Looper.getMainLooper()).post(() -> updateState(mPreference));
- }
- };
- mEthernetManager.addListener(mEthernetListener);
+ mEthernetListener = (iface, isAvailable) -> updateState(mPreference);
+ final Handler handler = new Handler(Looper.getMainLooper());
+ // Executor will execute to post the updateState event to a new handler which is created
+ // from the main looper when the {@link EthernetManager.Listener.onAvailabilityChanged}
+ // is triggerd.
+ mEthernetManager.addListener(mEthernetListener, r -> handler.post(r));
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
diff --git a/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java b/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java
index 5b02b3b0c18..d46d31a4860 100644
--- a/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java
+++ b/src/com/android/settings/notification/zen/ZenModeScheduleRuleSettings.java
@@ -42,7 +42,6 @@ import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.core.AbstractPreferenceController;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@@ -56,8 +55,7 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase {
public static final String ACTION = Settings.ACTION_ZEN_MODE_SCHEDULE_RULE_SETTINGS;
- // per-instance to ensure we're always using the current locale
- private final SimpleDateFormat mDayFormat = new SimpleDateFormat("EEE");
+ private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper();
private Preference mDays;
private TimePickerPreference mStart;
@@ -149,30 +147,11 @@ public class ZenModeScheduleRuleSettings extends ZenModeRuleSettingsBase {
}
private void updateDays() {
- // Compute an ordered, delimited list of day names based on the persisted user config.
- final int[] days = mSchedule.days;
- if (days != null && days.length > 0) {
- final StringBuilder sb = new StringBuilder();
- final Calendar c = Calendar.getInstance();
- int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c);
- for (int i = 0; i < daysOfWeek.length; i++) {
- final int day = daysOfWeek[i];
- for (int j = 0; j < days.length; j++) {
- if (day == days[j]) {
- c.set(Calendar.DAY_OF_WEEK, day);
- if (sb.length() > 0) {
- sb.append(mContext.getString(R.string.summary_divider_text));
- }
- sb.append(mDayFormat.format(c.getTime()));
- break;
- }
- }
- }
- if (sb.length() > 0) {
- mDays.setSummary(sb);
- mDays.notifyDependencyChange(false);
- return;
- }
+ String desc = mScheduleHelper.getDaysDescription(mContext, mSchedule);
+ if (desc != null) {
+ mDays.setSummary(desc);
+ mDays.notifyDependencyChange(false);
+ return;
}
mDays.setSummary(R.string.zen_mode_schedule_rule_days_none);
mDays.notifyDependencyChange(true);
diff --git a/src/com/android/settings/notification/zen/ZenRulePreference.java b/src/com/android/settings/notification/zen/ZenRulePreference.java
index a265a0776d2..8cf3106200b 100644
--- a/src/com/android/settings/notification/zen/ZenRulePreference.java
+++ b/src/com/android/settings/notification/zen/ZenRulePreference.java
@@ -23,22 +23,20 @@ import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.service.notification.ZenModeConfig;
-import android.view.View;
-import android.widget.CheckBox;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
import androidx.fragment.app.Fragment;
import androidx.preference.Preference;
-import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settings.utils.ManagedServiceSettings;
import com.android.settings.utils.ZenServiceListing;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
-import com.android.settingslib.widget.TwoTargetPreference;
import java.util.Map;
-public class ZenRulePreference extends TwoTargetPreference {
+public class ZenRulePreference extends PrimarySwitchPreference {
private static final ManagedServiceSettings.Config CONFIG =
ZenModeAutomationSettings.getConditionProviderConfig();
final String mId;
@@ -53,14 +51,13 @@ public class ZenRulePreference extends TwoTargetPreference {
CharSequence mName;
private Intent mIntent;
- private boolean mChecked;
- private CheckBox mCheckBox;
+
+ private final ZenRuleScheduleHelper mScheduleHelper = new ZenRuleScheduleHelper();
public ZenRulePreference(Context context,
final Map.Entry ruleEntry,
Fragment parent, MetricsFeatureProvider metricsProvider) {
super(context);
- setLayoutResource(R.layout.preference_checkable_two_target);
mBackend = ZenModeBackend.getInstance(context);
mContext = context;
mRule = ruleEntry.getValue();
@@ -72,50 +69,11 @@ public class ZenRulePreference extends TwoTargetPreference {
mServiceListing.reloadApprovedServices();
mPref = this;
mMetricsFeatureProvider = metricsProvider;
- mChecked = mRule.isEnabled();
setAttributes(mRule);
setWidgetLayoutResource(getSecondTargetResId());
- }
- protected int getSecondTargetResId() {
- if (mIntent != null) {
- return R.layout.zen_rule_widget;
- }
- return 0;
- }
-
- @Override
- public void onBindViewHolder(PreferenceViewHolder view) {
- super.onBindViewHolder(view);
- View settingsWidget = view.findViewById(android.R.id.widget_frame);
- View divider = view.findViewById(R.id.two_target_divider);
- if (mIntent != null) {
- divider.setVisibility(View.VISIBLE);
- settingsWidget.setVisibility(View.VISIBLE);
- settingsWidget.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mContext.startActivity(mIntent);
- }
- });
- } else {
- divider.setVisibility(View.GONE);
- settingsWidget.setVisibility(View.GONE);
- settingsWidget.setOnClickListener(null);
- }
-
- View checkboxContainer = view.findViewById(R.id.checkbox_container);
- if (checkboxContainer != null) {
- checkboxContainer.setOnClickListener(mOnCheckBoxClickListener);
- }
- mCheckBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox);
- if (mCheckBox != null) {
- mCheckBox.setChecked(mChecked);
- }
- }
-
- public boolean isChecked() {
- return mChecked;
+ // initialize the checked state of the preference
+ super.setChecked(mRule.isEnabled());
}
public void updatePreference(AutomaticZenRule rule) {
@@ -126,34 +84,24 @@ public class ZenRulePreference extends TwoTargetPreference {
if (mRule.isEnabled() != rule.isEnabled()) {
setChecked(rule.isEnabled());
- setSummary(computeRuleSummary(rule));
}
-
+ setSummary(computeRuleSummary(rule));
mRule = rule;
}
@Override
public void onClick() {
- mOnCheckBoxClickListener.onClick(null);
+ mContext.startActivity(mIntent);
}
- private void setChecked(boolean checked) {
- mChecked = checked;
- if (mCheckBox != null) {
- mCheckBox.setChecked(checked);
- }
+ @Override
+ public void setChecked(boolean checked) {
+ mRule.setEnabled(checked);
+ mBackend.updateZenRule(mId, mRule);
+ setAttributes(mRule);
+ super.setChecked(checked);
}
- private View.OnClickListener mOnCheckBoxClickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- mRule.setEnabled(!mChecked);
- mBackend.updateZenRule(mId, mRule);
- setChecked(mRule.isEnabled());
- setAttributes(mRule);
- }
- };
-
protected void setAttributes(AutomaticZenRule rule) {
final boolean isSchedule = ZenModeConfig.isValidScheduleConditionId(
rule.getConditionId(), true);
@@ -178,6 +126,30 @@ public class ZenRulePreference extends TwoTargetPreference {
}
private String computeRuleSummary(AutomaticZenRule rule) {
+ if (rule != null) {
+ // handle schedule-based rules
+ ScheduleInfo schedule =
+ ZenModeConfig.tryParseScheduleConditionId(rule.getConditionId());
+ if (schedule != null) {
+ String desc = mScheduleHelper.getDaysAndTimeSummary(mContext, schedule);
+ return (desc != null) ? desc :
+ mContext.getResources().getString(
+ R.string.zen_mode_schedule_rule_days_none);
+ }
+
+ // handle event-based rules
+ ZenModeConfig.EventInfo event =
+ ZenModeConfig.tryParseEventConditionId(rule.getConditionId());
+ if (event != null) {
+ if (event.calName != null) {
+ return event.calName;
+ } else {
+ return mContext.getResources().getString(
+ R.string.zen_mode_event_rule_calendar_any);
+ }
+ }
+ }
+
return (rule == null || !rule.isEnabled())
? mContext.getResources().getString(R.string.switch_off_text)
: mContext.getResources().getString(R.string.switch_on_text);
diff --git a/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java b/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java
new file mode 100644
index 00000000000..9a4f108437a
--- /dev/null
+++ b/src/com/android/settings/notification/zen/ZenRuleScheduleHelper.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.zen;
+
+import android.content.Context;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.text.format.DateFormat;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * Helper class for shared functionality regarding descriptions of custom zen rule schedules.
+ */
+public class ZenRuleScheduleHelper {
+ // per-instance to ensure we're always using the current locale
+ private SimpleDateFormat mDayFormat;
+
+ // Default constructor, which will use the current locale.
+ public ZenRuleScheduleHelper() {
+ mDayFormat = new SimpleDateFormat("EEE");
+ }
+
+ // Constructor for tests to provide an explicit locale
+ @VisibleForTesting
+ public ZenRuleScheduleHelper(Locale locale) {
+ mDayFormat = new SimpleDateFormat("EEE", locale);
+ }
+
+ /**
+ * Returns an ordered, comma-separated list of the days that a schedule applies, or null if no
+ * days.
+ */
+ public String getDaysDescription(Context context, ScheduleInfo schedule) {
+ // Compute an ordered, delimited list of day names based on the persisted user config.
+ final int[] days = schedule.days;
+ if (days != null && days.length > 0) {
+ final StringBuilder sb = new StringBuilder();
+ final Calendar c = Calendar.getInstance();
+ int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(c);
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ final int day = daysOfWeek[i];
+ for (int j = 0; j < days.length; j++) {
+ if (day == days[j]) {
+ c.set(Calendar.DAY_OF_WEEK, day);
+ if (sb.length() > 0) {
+ sb.append(context.getString(R.string.summary_divider_text));
+ }
+ sb.append(mDayFormat.format(c.getTime()));
+ break;
+ }
+ }
+ }
+
+ if (sb.length() > 0) {
+ return sb.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns an ordered summarized list of the days on which this schedule applies, with
+ * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
+ */
+ public String getShortDaysSummary(Context context, ScheduleInfo schedule) {
+ // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
+ // "Sun-Mon,Wed,Fri"
+ final int[] days = schedule.days;
+ if (days != null && days.length > 0) {
+ final StringBuilder sb = new StringBuilder();
+ final Calendar cStart = Calendar.getInstance();
+ final Calendar cEnd = Calendar.getInstance();
+ int[] daysOfWeek = ZenModeScheduleDaysSelection.getDaysOfWeekForLocale(cStart);
+ // the i for loop goes through days in order as determined by locale. as we walk through
+ // the days of the week, keep track of "start" and "last seen" as indicators for
+ // what's contiguous, and initialize them to something not near actual indices
+ int startDay = Integer.MIN_VALUE;
+ int lastSeenDay = Integer.MIN_VALUE;
+ for (int i = 0; i < daysOfWeek.length; i++) {
+ final int day = daysOfWeek[i];
+
+ // by default, output if this day is *not* included in the schedule, and thus
+ // ends a previously existing block. if this day is included in the schedule
+ // after all (as will be determined in the inner for loop), then output will be set
+ // to false.
+ boolean output = (i == lastSeenDay + 1);
+ for (int j = 0; j < days.length; j++) {
+ if (day == days[j]) {
+ // match for this day in the schedule (indicated by counter i)
+ if (i == lastSeenDay + 1) {
+ // contiguous to the block we're walking through right now, record it
+ // (specifically, i, the day index) and move on to the next day
+ lastSeenDay = i;
+ output = false;
+ } else {
+ // it's a match, but not 1 past the last match, we are starting a new
+ // block
+ startDay = i;
+ lastSeenDay = i;
+ }
+
+ // if there is a match on the last day, also make sure to output at the end
+ // of this loop, and mark the day as the last day we'll have seen in the
+ // scheduled days.
+ if (i == daysOfWeek.length - 1) {
+ output = true;
+ }
+ break;
+ }
+ }
+
+ // output in either of 2 cases: this day is not a match, so has ended any previous
+ // block, or this day *is* a match but is the last day of the week, so we need to
+ // summarize
+ if (output) {
+ // either describe just the single day if startDay == lastSeenDay, or
+ // output "startDay - lastSeenDay" as a group
+ if (sb.length() > 0) {
+ sb.append(context.getString(R.string.summary_divider_text));
+ }
+
+ if (startDay == lastSeenDay) {
+ // last group was only one day
+ cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
+ sb.append(mDayFormat.format(cStart.getTime()));
+ } else {
+ // last group was a contiguous group of days, so group them together
+ cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
+ cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
+ sb.append(context.getString(R.string.summary_range_symbol_combination,
+ mDayFormat.format(cStart.getTime()),
+ mDayFormat.format(cEnd.getTime())));
+ }
+ }
+ }
+
+ if (sb.length() > 0) {
+ return sb.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Convenience method for representing the specified time in string format.
+ */
+ private String timeString(Context context, int hour, int minute) {
+ final Calendar c = Calendar.getInstance();
+ c.set(Calendar.HOUR_OF_DAY, hour);
+ c.set(Calendar.MINUTE, minute);
+ return DateFormat.getTimeFormat(context).format(c.getTime());
+ }
+
+ /**
+ * Combination description for a zen rule schedule including both day summary and time bounds.
+ */
+ public String getDaysAndTimeSummary(Context context, ScheduleInfo schedule) {
+ final StringBuilder sb = new StringBuilder();
+ String daysSummary = getShortDaysSummary(context, schedule);
+ if (daysSummary == null) {
+ // no use outputting times without dates
+ return null;
+ }
+ sb.append(daysSummary);
+ sb.append(context.getString(R.string.summary_divider_text));
+ sb.append(context.getString(R.string.summary_range_symbol_combination,
+ timeString(context, schedule.startHour, schedule.startMinute),
+ timeString(context, schedule.endHour, schedule.endMinute)));
+
+ return sb.toString();
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragmentTest.java b/tests/robotests/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragmentTest.java
new file mode 100644
index 00000000000..c6c6a66b986
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/inputmethod/AvailableVirtualKeyboardFragmentTest.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 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.inputmethod;
+
+import static com.android.settings.dashboard.profileselector.ProfileSelectFragment.EXTRA_PROFILE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.provider.SearchIndexableResource;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.profileselector.ProfileSelectFragment;
+import com.android.settings.testutils.shadow.ShadowInputMethodManagerWithMethodList;
+import com.android.settings.testutils.shadow.ShadowSecureSettings;
+import com.android.settingslib.inputmethod.InputMethodPreference;
+import com.android.settingslib.inputmethod.InputMethodSettingValuesWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {
+ ShadowSecureSettings.class,
+ ShadowInputMethodManagerWithMethodList.class
+})
+public class AvailableVirtualKeyboardFragmentTest {
+
+ @Mock
+ private InputMethodManager mInputMethodManager;
+ @Mock
+ private InputMethodSettingValuesWrapper mValuesWrapper;
+ @Mock
+ private PreferenceScreen mPreferenceScreen;
+ @Mock
+ private PreferenceManager mPreferenceManager;
+ @Mock
+ private InputMethodPreference mInputMethodPreference;
+ private Context mContext;
+ private AvailableVirtualKeyboardFragment mFragment;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ initFragment();
+ initMock();
+ }
+
+ @Test
+ public void onCreatePreferences_shouldAddResource() {
+ mFragment.onAttach(mContext);
+
+ mFragment.onCreatePreferences(new Bundle(), "test");
+
+ verify(mFragment).addPreferencesFromResource(R.xml.available_virtual_keyboard);
+ }
+
+ @Test
+ public void onResume_refreshAllInputMethodAndSubtypes() {
+ mFragment.onAttach(mContext);
+
+ mFragment.onResume();
+
+ // One invocation is in onResume(), another is in updateInputMethodPreferenceViews().
+ verify(mValuesWrapper, times(2)).refreshAllInputMethodAndSubtypes();
+ }
+
+ @Test
+ public void onResume_updateInputMethodPreferenceViews() {
+ mFragment.onAttach(mContext);
+
+ mFragment.onResume();
+
+ verify(mFragment).updateInputMethodPreferenceViews();
+ }
+
+ @Test
+ public void onSaveInputMethodPreference_refreshAllInputMethodAndSubtypes() {
+ mFragment.onAttach(mContext);
+
+ mFragment.onSaveInputMethodPreference(mInputMethodPreference);
+
+ verify(mValuesWrapper).refreshAllInputMethodAndSubtypes();
+ }
+
+ @Test
+ public void updateInputMethodPreferenceViews_callsExpectedMethods() {
+ mFragment.onAttach(mContext);
+
+ mFragment.updateInputMethodPreferenceViews();
+
+ verify(mValuesWrapper).getInputMethodList();
+ verify(mInputMethodManager).getEnabledInputMethodListAsUser(anyInt());
+ }
+
+ @Test
+ public void updateInputMethodPreferenceViews_addExpectedInputMethodPreference() {
+ final int inputMethodNums = 5;
+ mFragment.onAttach(mContext);
+ when(mValuesWrapper.getInputMethodList()).thenReturn(createFakeInputMethodInfoList(
+ "test", inputMethodNums));
+
+ mFragment.updateInputMethodPreferenceViews();
+
+ assertThat(mFragment.mInputMethodPreferenceList).hasSize(inputMethodNums);
+ }
+
+ @Test
+ public void searchIndexProvider_shouldIndexResource() {
+ final List indexRes =
+ AvailableVirtualKeyboardFragment.SEARCH_INDEX_DATA_PROVIDER
+ .getXmlResourcesToIndex(RuntimeEnvironment.application, true /* enabled */);
+
+ assertThat(indexRes).isNotNull();
+ assertThat(indexRes.get(0).xmlResId).isEqualTo(mFragment.getPreferenceScreenResId());
+ }
+
+ private void initFragment() {
+ final Bundle bundle = new Bundle();
+ bundle.putInt(EXTRA_PROFILE, ProfileSelectFragment.ProfileType.PERSONAL);
+ mFragment = spy(new AvailableVirtualKeyboardFragment());
+ mFragment.setArguments(bundle);
+ mFragment.mInputMethodSettingValues = mValuesWrapper;
+ ReflectionHelpers.setField(mFragment, "mPreferenceManager", mPreferenceManager);
+ }
+
+ private void initMock() {
+ when(mFragment.getContext()).thenReturn(mContext);
+ when(mFragment.getPreferenceScreen()).thenReturn(mPreferenceScreen);
+ when(mPreferenceManager.getContext()).thenReturn(mContext);
+ when(mContext.getSystemService(InputMethodManager.class)).thenReturn(mInputMethodManager);
+ }
+
+ private List createFakeInputMethodInfoList(final String name, int num) {
+ List subtypes = new ArrayList<>();
+
+ subtypes.add(new InputMethodSubtype.InputMethodSubtypeBuilder()
+ .build());
+ subtypes.add(new InputMethodSubtype.InputMethodSubtypeBuilder()
+ .build());
+
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = new ServiceInfo();
+ resolveInfo.serviceInfo.packageName = "com.android.ime";
+ resolveInfo.serviceInfo.name = name;
+ resolveInfo.serviceInfo.applicationInfo = new ApplicationInfo();
+ resolveInfo.serviceInfo.applicationInfo.enabled = true;
+
+ List inputMethodInfoList = new ArrayList<>();
+ for (int i = 0; i < num; i++) {
+ inputMethodInfoList.add(new InputMethodInfo(
+ resolveInfo,
+ false /* isAuxIme */,
+ "TestSettingsActivity",
+ subtypes,
+ 0 /* isDefaultResId */,
+ true /* forceDefault */));
+ }
+ return inputMethodInfoList;
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
index 65be91f71f4..df38e7f72a0 100644
--- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
+++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
@@ -246,7 +246,6 @@ public class SettingsSliceProviderTest {
}
@Test
- @Ignore
@Config(shadows = ShadowStrictMode.class)
public void onBindSlice_backgroundThread_shouldOverrideStrictMode() {
ShadowThreadUtils.setIsMainThread(false);
diff --git a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java
index 4e968a29c70..9ace6bb0cf0 100644
--- a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java
+++ b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java
@@ -21,13 +21,14 @@ import android.net.Uri;
import android.net.wifi.WifiManager;
import android.provider.Settings;
+import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.slices.SliceBackgroundWorker;
public class FakeToggleController extends TogglePreferenceController {
public static final String AVAILABILITY_KEY = "fake_toggle_availability_key";
- public static final int HIGHLIGHT_MENU_RES = 5678;
+ public static final int HIGHLIGHT_MENU_RES = R.string.menu_key_about_device;
public static final IntentFilter INTENT_FILTER = new IntentFilter(
WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
diff --git a/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java b/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java
new file mode 100644
index 00000000000..2faee080c6e
--- /dev/null
+++ b/tests/unit/src/com/android/settings/notification/zen/ZenRuleScheduleHelperTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.zen;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.LocaleList;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Calendar;
+import java.util.Locale;
+
+@RunWith(AndroidJUnit4.class)
+public class ZenRuleScheduleHelperTest {
+ private ZenRuleScheduleHelper mScheduleHelper;
+ private ScheduleInfo mScheduleInfo;
+
+ private Context mContext;
+
+ @Mock
+ private Resources mResources;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ // explicitly initialize to Locale.US just for ease of explicitly testing the string values
+ // of the days of the week if the test locale doesn't happen to be in the US
+ mScheduleHelper = new ZenRuleScheduleHelper(Locale.US);
+ mScheduleInfo = new ScheduleInfo();
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ when(mContext.getResources()).thenReturn(mResources);
+
+ // Resources will be called upon to join strings together, either to get a divider
+ // or a combination of two strings. Conveniently, these have different signatures.
+ // Divider method calls getString(string divider id)
+ when(mResources.getString(anyInt())).thenReturn(",");
+
+ // Combination method calls getString(combination id, first item, second item)
+ // and returns "first - second"
+ when(mResources.getString(anyInt(), anyString(), anyString())).thenAnswer(
+ invocation -> {
+ return invocation.getArgument(1).toString() // first item
+ + "-"
+ + invocation.getArgument(2).toString(); // second item
+ });
+
+ // for locale used in time format
+ Configuration config = new Configuration();
+ config.setLocales(new LocaleList(Locale.US));
+ when(mResources.getConfiguration()).thenReturn(config);
+ }
+
+ @Test
+ public void getDaysDescription() {
+ // Test various cases of where the days are set.
+ // No days
+ mScheduleInfo.days = new int[] {};
+ assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isNull();
+
+ // one day
+ mScheduleInfo.days = new int[] {Calendar.FRIDAY};
+ assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo)).isEqualTo("Fri");
+
+ // Monday through Friday
+ mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY,
+ Calendar.THURSDAY, Calendar.FRIDAY};
+ assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo))
+ .isEqualTo("Mon,Tue,Wed,Thu,Fri");
+
+ // Some scattered days of the week
+ mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.WEDNESDAY, Calendar.THURSDAY,
+ Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getDaysDescription(mContext, mScheduleInfo))
+ .isEqualTo("Sun,Wed,Thu,Sat");
+ }
+
+ @Test
+ public void getShortDaysSummary_noOrSingleDays() {
+ // Test various cases for grouping and not-grouping of days.
+ // No days
+ mScheduleInfo.days = new int[]{};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isNull();
+
+ // A single day at the beginning of the week
+ mScheduleInfo.days = new int[]{Calendar.SUNDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sun");
+
+ // A single day in the middle of the week
+ mScheduleInfo.days = new int[]{Calendar.THURSDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Thu");
+
+ // A single day at the end of the week
+ mScheduleInfo.days = new int[]{Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo)).isEqualTo("Sat");
+ }
+
+ @Test
+ public void getShortDaysSummary_oneGroup() {
+ // The whole week
+ mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Sun-Sat");
+
+ // Various cases of one big group
+ // Sunday through Thursday
+ mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Sun-Thu");
+
+ // Wednesday through Saturday
+ mScheduleInfo.days = new int[] {Calendar.WEDNESDAY, Calendar.THURSDAY,
+ Calendar.FRIDAY, Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Wed-Sat");
+
+ // Monday through Friday
+ mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Mon-Fri");
+ }
+
+ @Test
+ public void getShortDaysSummary_mixed() {
+ // cases combining groups and single days scattered around
+ mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Sun,Tue-Thu,Sat");
+
+ mScheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+ Calendar.WEDNESDAY, Calendar.FRIDAY, Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Sun-Wed,Fri-Sat");
+
+ mScheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.WEDNESDAY,
+ Calendar.FRIDAY, Calendar.SATURDAY};
+ assertThat(mScheduleHelper.getShortDaysSummary(mContext, mScheduleInfo))
+ .isEqualTo("Mon,Wed,Fri-Sat");
+ }
+
+ @Test
+ public void getDaysAndTimeSummary() {
+ // Combination days & time settings
+ // No days, no output, even if the times are set.
+ mScheduleInfo.startHour = 10;
+ mScheduleInfo.endHour = 16;
+ mScheduleInfo.days = new int[]{};
+ assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo)).isNull();
+
+ // If there are days then they are combined with the time combination
+ mScheduleInfo.days = new int[]{Calendar.SUNDAY, Calendar.MONDAY, Calendar.WEDNESDAY};
+ assertThat(mScheduleHelper.getDaysAndTimeSummary(mContext, mScheduleInfo))
+ .isEqualTo("Sun-Mon,Wed,10:00 AM-4:00 PM");
+ }
+}
diff --git a/tests/unit/src/com/android/settings/testutils/FakeToggleController.java b/tests/unit/src/com/android/settings/testutils/FakeToggleController.java
index c232479d61c..a7e7320a96d 100644
--- a/tests/unit/src/com/android/settings/testutils/FakeToggleController.java
+++ b/tests/unit/src/com/android/settings/testutils/FakeToggleController.java
@@ -21,13 +21,14 @@ import android.net.Uri;
import android.net.wifi.WifiManager;
import android.provider.Settings;
+import com.android.settings.R;
import com.android.settings.core.TogglePreferenceController;
import com.android.settings.slices.SliceBackgroundWorker;
public class FakeToggleController extends TogglePreferenceController {
public static final String AVAILABILITY_KEY = "fake_toggle_availability_key";
- public static final int HIGHLIGHT_MENU_RES = 5678;
+ public static final int HIGHLIGHT_MENU_RES = R.string.menu_key_about_device;
public static final IntentFilter INTENT_FILTER = new IntentFilter(
WifiManager.WIFI_AP_STATE_CHANGED_ACTION);