Add mode: Support for app-provided modes
(This completes the add-mode flow except for the choose-a-name-and-icon step for custom modes). Bug: 326442408 Flag: android.app.modes_ui Test: atest com.android.settings.notification.modes Change-Id: I7aceec01ed54d804bcac53d932277c243c1f81bf
This commit is contained in:
25
res/drawable/ic_zen_mode_new_option_custom.xml
Normal file
25
res/drawable/ic_zen_mode_new_option_custom.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:viewportHeight="960"
|
||||
android:viewportWidth="960">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M620,440Q645,440 662.5,422.5Q680,405 680,380Q680,355 662.5,337.5Q645,320 620,320Q595,320 577.5,337.5Q560,355 560,380Q560,405 577.5,422.5Q595,440 620,440ZM340,440Q365,440 382.5,422.5Q400,405 400,380Q400,355 382.5,337.5Q365,320 340,320Q315,320 297.5,337.5Q280,355 280,380Q280,405 297.5,422.5Q315,440 340,440ZM480,700Q548,700 603.5,661.5Q659,623 684,560L618,560Q596,597 559.5,618.5Q523,640 480,640Q437,640 400.5,618.5Q364,597 342,560L276,560Q301,623 356.5,661.5Q412,700 480,700ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800Z" />
|
||||
</vector>
|
@@ -8022,6 +8022,12 @@
|
||||
<!-- Priority Modes: Indicates that a mode is disabled by the user. [CHAR_LIMIT=40] -->
|
||||
<string name="zen_mode_disabled_by_user">Disabled</string>
|
||||
|
||||
<!-- Priority Modes: Title of the "Create a mode" dialog, to choose the mode type. [CHAR_LIMIT=30] -->
|
||||
<string name="zen_mode_new_title">Create a mode</string>
|
||||
|
||||
<!-- Priority Modes: Option to add a "custom" mode in the "Add a mode" dialog. [CHAR_LIMIT=20] -->
|
||||
<string name="zen_mode_new_option_custom">Custom</string>
|
||||
|
||||
<!-- Subtitle for the Do not Disturb slice. [CHAR LIMIT=50]-->
|
||||
<string name="zen_mode_slice_subtitle">Limit interruptions</string>
|
||||
|
||||
|
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
class ConfigurationActivityHelper {
|
||||
|
||||
private static final String TAG = "ConfigurationActivityHelper";
|
||||
|
||||
private final PackageManager mPm;
|
||||
|
||||
ConfigurationActivityHelper(PackageManager pm) {
|
||||
mPm = pm;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Intent getConfigurationActivityIntentForMode(ZenMode zenMode,
|
||||
Function<ComponentName, ComponentInfo> approvedServiceFinder) {
|
||||
|
||||
String owner = zenMode.getRule().getPackageName();
|
||||
ComponentName configActivity = null;
|
||||
if (zenMode.getRule().getConfigurationActivity() != null) {
|
||||
// If a configuration activity is present, use that directly in the intent
|
||||
configActivity = zenMode.getRule().getConfigurationActivity();
|
||||
} else {
|
||||
// Otherwise, look for a condition provider service for the rule's package
|
||||
ComponentInfo ci = approvedServiceFinder.apply(zenMode.getRule().getOwner());
|
||||
if (ci != null) {
|
||||
configActivity = extractConfigurationActivityFromComponent(ci);
|
||||
}
|
||||
}
|
||||
|
||||
if (configActivity != null
|
||||
&& (owner == null || isSameOwnerPackage(owner, configActivity))
|
||||
&& isResolvableActivity(configActivity)) {
|
||||
return new Intent()
|
||||
.setComponent(configActivity)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(ConditionProviderService.EXTRA_RULE_ID, zenMode.getId())
|
||||
.putExtra(EXTRA_AUTOMATIC_RULE_ID, zenMode.getId());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
ComponentName getConfigurationActivityFromApprovedComponent(ComponentInfo ci) {
|
||||
ComponentName configActivity = extractConfigurationActivityFromComponent(ci);
|
||||
if (configActivity != null
|
||||
&& isSameOwnerPackage(ci.packageName, configActivity)
|
||||
&& isResolvableActivity(configActivity)) {
|
||||
return configActivity;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the {@link ComponentName} corresponding to the mode configuration <em>activity</em>
|
||||
* from the component declaring the rule (which may be the Activity itself, or a CPS that points
|
||||
* to the activity in question in its metadata).
|
||||
*
|
||||
* <p>This method doesn't perform any validation, so the activity may or may not exist.
|
||||
*/
|
||||
@Nullable
|
||||
private ComponentName extractConfigurationActivityFromComponent(ComponentInfo ci) {
|
||||
if (ci instanceof ActivityInfo) {
|
||||
// New (activity-backed) rule.
|
||||
return new ComponentName(ci.packageName, ci.name);
|
||||
} else if (ci.metaData != null) {
|
||||
// Old (service-backed) rule.
|
||||
final String configurationActivity = ci.metaData.getString(
|
||||
ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY);
|
||||
if (configurationActivity != null) {
|
||||
return ComponentName.unflattenFromString(configurationActivity);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the activity is the same package as the rule owner.
|
||||
*/
|
||||
private boolean isSameOwnerPackage(String ownerPkg, ComponentName activityName) {
|
||||
try {
|
||||
int ownerUid = mPm.getPackageUid(ownerPkg, 0);
|
||||
int configActivityOwnerUid = mPm.getPackageUid(activityName.getPackageName(), 0);
|
||||
if (ownerUid == configActivityOwnerUid) {
|
||||
return true;
|
||||
} else {
|
||||
Log.w(TAG, String.format("Config activity (%s) not in owner package (%s)",
|
||||
activityName, ownerPkg));
|
||||
return false;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find config activity " + activityName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Verifies that the activity exists and hasn't been disabled. */
|
||||
private boolean isResolvableActivity(ComponentName activityName) {
|
||||
Intent intent = new Intent().setComponent(activityName);
|
||||
List<ResolveInfo> results = mPm.queryIntentActivities(intent, /* flags= */ 0);
|
||||
|
||||
if (intent.resolveActivity(mPm) == null || results.isEmpty()) {
|
||||
Log.w(TAG, "Cannot resolve: " + activityName);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -62,8 +62,7 @@ public class ZenModeFragment extends ZenModeFragmentBase {
|
||||
prefControllers.add(new ZenModeDisplayLinkPreferenceController(
|
||||
context, "mode_display_settings", mBackend, mHelperBackend));
|
||||
prefControllers.add(new ZenModeSetTriggerLinkPreferenceController(context,
|
||||
"zen_automatic_trigger_category", this, mBackend,
|
||||
context.getPackageManager()));
|
||||
"zen_automatic_trigger_category", this, mBackend));
|
||||
prefControllers.add(new InterruptionFilterPreferenceController(
|
||||
context, "allow_filtering", mBackend));
|
||||
prefControllers.add(new ManualDurationPreferenceController(
|
||||
|
@@ -18,20 +18,12 @@ package com.android.settings.notification.modes;
|
||||
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
|
||||
import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
@@ -39,14 +31,10 @@ import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.utils.ManagedServiceSettings;
|
||||
import com.android.settings.utils.ZenServiceListing;
|
||||
import com.android.settingslib.PrimarySwitchPreference;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Preference controller for the link to an individual mode's configuration page.
|
||||
*/
|
||||
@@ -56,23 +44,25 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc
|
||||
@VisibleForTesting
|
||||
protected static final String AUTOMATIC_TRIGGER_PREF_KEY = "zen_automatic_trigger_settings";
|
||||
|
||||
private static final ManagedServiceSettings.Config CONFIG =
|
||||
ZenModesListFragment.getConditionProviderConfig();
|
||||
|
||||
private ZenServiceListing mServiceListing;
|
||||
private final PackageManager mPm;
|
||||
private final ConfigurationActivityHelper mConfigurationActivityHelper;
|
||||
private final ZenServiceListing mServiceListing;
|
||||
private final DashboardFragment mFragment;
|
||||
|
||||
ZenModeSetTriggerLinkPreferenceController(Context context, String key,
|
||||
DashboardFragment fragment, ZenModesBackend backend,
|
||||
PackageManager packageManager) {
|
||||
super(context, key, backend);
|
||||
mFragment = fragment;
|
||||
mPm = packageManager;
|
||||
DashboardFragment fragment, ZenModesBackend backend) {
|
||||
this(context, key, fragment, backend,
|
||||
new ConfigurationActivityHelper(context.getPackageManager()),
|
||||
new ZenServiceListing(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void setServiceListing(ZenServiceListing serviceListing) {
|
||||
ZenModeSetTriggerLinkPreferenceController(Context context, String key,
|
||||
DashboardFragment fragment, ZenModesBackend backend,
|
||||
ConfigurationActivityHelper configurationActivityHelper,
|
||||
ZenServiceListing serviceListing) {
|
||||
super(context, key, backend);
|
||||
mFragment = fragment;
|
||||
mConfigurationActivityHelper = configurationActivityHelper;
|
||||
mServiceListing = serviceListing;
|
||||
}
|
||||
|
||||
@@ -83,11 +73,9 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc
|
||||
|
||||
@Override
|
||||
public void displayPreference(PreferenceScreen screen, @NonNull ZenMode zenMode) {
|
||||
if (mServiceListing == null) {
|
||||
mServiceListing = new ZenServiceListing(
|
||||
mContext, CONFIG, zenMode.getRule().getPackageName());
|
||||
}
|
||||
mServiceListing.reloadApprovedServices();
|
||||
// Preload approved components, but only for the package that owns the rule (since it's the
|
||||
// only package that can have a valid configurationActivity).
|
||||
mServiceListing.loadApprovedComponents(zenMode.getRule().getPackageName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,8 +118,9 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Intent intent = getAppRuleIntent(zenMode);
|
||||
if (intent != null && isValidIntent(intent)) {
|
||||
Intent intent = mConfigurationActivityHelper.getConfigurationActivityIntentForMode(
|
||||
zenMode, mServiceListing::findService);
|
||||
if (intent != null) {
|
||||
preference.setVisible(true);
|
||||
switchPref.setTitle(R.string.zen_mode_configuration_link_title);
|
||||
switchPref.setSummary(zenMode.getRule().getTriggerDescription());
|
||||
@@ -161,68 +150,4 @@ class ZenModeSetTriggerLinkPreferenceController extends AbstractZenModePreferenc
|
||||
});
|
||||
// TODO: b/342156843 - Do we want to jump to the corresponding schedule editing screen?
|
||||
};
|
||||
|
||||
@VisibleForTesting
|
||||
protected @Nullable Intent getAppRuleIntent(ZenMode zenMode) {
|
||||
Intent intent = new Intent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.putExtra(ConditionProviderService.EXTRA_RULE_ID, zenMode.getId())
|
||||
.putExtra(EXTRA_AUTOMATIC_RULE_ID, zenMode.getId());
|
||||
String owner = zenMode.getRule().getPackageName();
|
||||
ComponentName configActivity = null;
|
||||
if (zenMode.getRule().getConfigurationActivity() != null) {
|
||||
// If a configuration activity is present, use that directly in the intent
|
||||
configActivity = zenMode.getRule().getConfigurationActivity();
|
||||
} else {
|
||||
// Otherwise, look for a condition provider service for the rule's package
|
||||
ComponentInfo ci = mServiceListing.findService(zenMode.getRule().getOwner());
|
||||
if (ci == null) {
|
||||
// do nothing
|
||||
} else if (ci instanceof ActivityInfo) {
|
||||
// new activity backed rule
|
||||
intent.setComponent(new ComponentName(ci.packageName, ci.name));
|
||||
return intent;
|
||||
} else if (ci.metaData != null) {
|
||||
// old service backed rule
|
||||
final String configurationActivity = ci.metaData.getString(
|
||||
ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY);
|
||||
if (configurationActivity != null) {
|
||||
configActivity = ComponentName.unflattenFromString(configurationActivity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configActivity != null) {
|
||||
// verify that the owner of the rule owns the configuration activity, but only if
|
||||
// owner exists
|
||||
intent.setComponent(configActivity);
|
||||
if (owner == null) {
|
||||
return intent;
|
||||
}
|
||||
try {
|
||||
int ownerUid = mPm.getPackageUid(owner, 0);
|
||||
int configActivityOwnerUid = mPm.getPackageUid(configActivity.getPackageName(), 0);
|
||||
if (ownerUid == configActivityOwnerUid) {
|
||||
return intent;
|
||||
} else {
|
||||
Log.w(TAG, "Config activity not in owner package for "
|
||||
+ zenMode.getRule().getName());
|
||||
return null;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find config activity");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isValidIntent(Intent intent) {
|
||||
List<ResolveInfo> results = mPm.queryIntentActivities(
|
||||
intent, PackageManager.ResolveInfoFlags.of(0));
|
||||
if (intent.resolveActivity(mPm) == null || results.size() == 0) {
|
||||
Log.w(TAG, "intent for zen rule invalid: " + intent);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,8 @@ import android.os.UserManager;
|
||||
import android.provider.Settings.Global;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.dashboard.RestrictedDashboardFragment;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
|
||||
@@ -57,6 +59,11 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment {
|
||||
return TAG;
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
void setBackend(ZenModesBackend backend) {
|
||||
mBackend = backend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
mContext = context;
|
||||
|
@@ -16,27 +16,82 @@
|
||||
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import com.android.settings.utils.ZenServiceListing;
|
||||
import com.android.settings.R;
|
||||
import com.android.settingslib.Utils;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
|
||||
import java.util.Random;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
|
||||
class ZenModesListAddModePreferenceController extends AbstractPreferenceController {
|
||||
|
||||
private final ZenModesBackend mBackend;
|
||||
private final ZenServiceListing mServiceListing;
|
||||
private final OnAddModeListener mOnAddModeListener;
|
||||
|
||||
ZenModesListAddModePreferenceController(Context context, ZenModesBackend backend,
|
||||
ZenServiceListing serviceListing) {
|
||||
private final ConfigurationActivityHelper mConfigurationActivityHelper;
|
||||
private final NotificationManager mNotificationManager;
|
||||
private final PackageManager mPackageManager;
|
||||
private final Function<ApplicationInfo, Drawable> mAppIconRetriever;
|
||||
private final ListeningExecutorService mBackgroundExecutor;
|
||||
private final Executor mUiThreadExecutor;
|
||||
|
||||
record ModeType(String name, Drawable icon, @Nullable String summary,
|
||||
@Nullable Intent creationActivityIntent) { }
|
||||
|
||||
interface OnAddModeListener {
|
||||
void onAvailableModeTypesForAdd(List<ModeType> types);
|
||||
}
|
||||
|
||||
ZenModesListAddModePreferenceController(Context context, OnAddModeListener onAddModeListener) {
|
||||
this(context, onAddModeListener, new ZenServiceListing(context),
|
||||
new ConfigurationActivityHelper(context.getPackageManager()),
|
||||
context.getSystemService(NotificationManager.class), context.getPackageManager(),
|
||||
applicationInfo -> Utils.getBadgedIcon(context, applicationInfo),
|
||||
Executors.newCachedThreadPool(), context.getMainExecutor());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ZenModesListAddModePreferenceController(Context context,
|
||||
OnAddModeListener onAddModeListener, ZenServiceListing serviceListing,
|
||||
ConfigurationActivityHelper configurationActivityHelper,
|
||||
NotificationManager notificationManager, PackageManager packageManager,
|
||||
Function<ApplicationInfo, Drawable> appIconRetriever,
|
||||
ExecutorService backgroundExecutor, Executor uiThreadExecutor) {
|
||||
super(context);
|
||||
mBackend = backend;
|
||||
mOnAddModeListener = onAddModeListener;
|
||||
mServiceListing = serviceListing;
|
||||
mConfigurationActivityHelper = configurationActivityHelper;
|
||||
mNotificationManager = notificationManager;
|
||||
mPackageManager = packageManager;
|
||||
mAppIconRetriever = appIconRetriever;
|
||||
mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
|
||||
mUiThreadExecutor = uiThreadExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,12 +107,79 @@ class ZenModesListAddModePreferenceController extends AbstractPreferenceControll
|
||||
@Override
|
||||
public void updateState(Preference preference) {
|
||||
preference.setOnPreferenceClickListener(pref -> {
|
||||
// TODO: b/326442408 - Launch the proper mode creation flow (using mServiceListing).
|
||||
ZenMode mode = mBackend.addCustomMode("New mode #" + new Random().nextInt(1000));
|
||||
if (mode != null) {
|
||||
ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch();
|
||||
}
|
||||
onClickAddMode();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void onClickAddMode() {
|
||||
FutureUtil.whenDone(
|
||||
mBackgroundExecutor.submit(this::getModeProviders),
|
||||
mOnAddModeListener::onAvailableModeTypesForAdd,
|
||||
mUiThreadExecutor);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private ImmutableList<ModeType> getModeProviders() {
|
||||
ImmutableSet<ComponentInfo> approvedComponents = mServiceListing.loadApprovedComponents();
|
||||
|
||||
ArrayList<ModeType> appProvidedModes = new ArrayList<>();
|
||||
for (ComponentInfo ci: approvedComponents) {
|
||||
ModeType modeType = getValidNewModeTypeFromComponent(ci);
|
||||
if (modeType != null) {
|
||||
appProvidedModes.add(modeType);
|
||||
}
|
||||
}
|
||||
|
||||
return ImmutableList.<ModeType>builder()
|
||||
.add(new ModeType(
|
||||
mContext.getString(R.string.zen_mode_new_option_custom),
|
||||
mContext.getDrawable(R.drawable.ic_zen_mode_new_option_custom),
|
||||
null, null))
|
||||
.addAll(appProvidedModes.stream()
|
||||
.sorted(Comparator.comparing(ModeType::name))
|
||||
.toList())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ModeType} object corresponding to the approved {@link ComponentInfo} that
|
||||
* specifies a creatable rule, if such a mode can actually be created (has an associated and
|
||||
* enabled configuration activity, has not exceeded the rule instance limit, etc). Otherwise,
|
||||
* returns {@code null}.
|
||||
*/
|
||||
@WorkerThread
|
||||
@Nullable
|
||||
private ModeType getValidNewModeTypeFromComponent(ComponentInfo ci) {
|
||||
if (ci.metaData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String ruleType = (ci instanceof ServiceInfo)
|
||||
? ci.metaData.getString(ConditionProviderService.META_DATA_RULE_TYPE)
|
||||
: ci.metaData.getString(NotificationManager.META_DATA_AUTOMATIC_RULE_TYPE);
|
||||
if (ruleType == null || ruleType.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int ruleInstanceLimit = (ci instanceof ServiceInfo)
|
||||
? ci.metaData.getInt(ConditionProviderService.META_DATA_RULE_INSTANCE_LIMIT, -1)
|
||||
: ci.metaData.getInt(NotificationManager.META_DATA_RULE_INSTANCE_LIMIT, -1);
|
||||
if (ruleInstanceLimit > 0 && mNotificationManager.getRuleInstanceCount(
|
||||
ci.getComponentName()) >= ruleInstanceLimit) {
|
||||
return null; // Would exceed instance limit.
|
||||
}
|
||||
|
||||
ComponentName configurationActivity =
|
||||
mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(ci);
|
||||
if (configurationActivity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String appName = ci.applicationInfo.loadLabel(mPackageManager).toString();
|
||||
Drawable appIcon = mAppIconRetriever.apply(ci.applicationInfo);
|
||||
Intent configActivityIntent = new Intent().setComponent(configurationActivity);
|
||||
return new ModeType(ruleType, appIcon, appName, configActivityIntent);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ZenModesListAddModeTypeChooserDialog extends InstrumentedDialogFragment {
|
||||
|
||||
private static final String TAG = "ZenModesListAddModeTypeChooserDialog";
|
||||
|
||||
private OnChooseModeTypeListener mChooseModeTypeListener;
|
||||
private ImmutableList<ModeType> mOptions;
|
||||
|
||||
interface OnChooseModeTypeListener {
|
||||
void onTypeSelected(ModeType type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMetricsCategory() {
|
||||
// TODO: b/332937635 - Update metrics category
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void show(DashboardFragment parent,
|
||||
OnChooseModeTypeListener onChooseModeTypeListener,
|
||||
List<ModeType> options) {
|
||||
ZenModesListAddModeTypeChooserDialog dialog = new ZenModesListAddModeTypeChooserDialog();
|
||||
dialog.mChooseModeTypeListener = onChooseModeTypeListener;
|
||||
dialog.mOptions = ImmutableList.copyOf(options);
|
||||
dialog.setTargetFragment(parent, 0);
|
||||
dialog.show(parent.getParentFragmentManager(), TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
checkState(getContext() != null);
|
||||
return new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.zen_mode_new_title)
|
||||
.setAdapter(new OptionsAdapter(getContext(), mOptions),
|
||||
(dialog, which) -> mChooseModeTypeListener.onTypeSelected(
|
||||
mOptions.get(which)))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
private static class OptionsAdapter extends ArrayAdapter<ModeType> {
|
||||
|
||||
private final LayoutInflater mInflater;
|
||||
|
||||
private OptionsAdapter(Context context,
|
||||
ImmutableList<ModeType> availableModeProviders) {
|
||||
super(context, R.layout.zen_mode_type_item, availableModeProviders);
|
||||
mInflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = mInflater.inflate(R.layout.zen_mode_type_item, parent, false);
|
||||
}
|
||||
ImageView imageView = checkNotNull(convertView.findViewById(R.id.icon));
|
||||
TextView title = checkNotNull(convertView.findViewById(R.id.title));
|
||||
TextView subtitle = checkNotNull(convertView.findViewById(R.id.subtitle));
|
||||
|
||||
ModeType option = checkNotNull(getItem(position));
|
||||
imageView.setImageDrawable(option.icon());
|
||||
title.setText(option.name());
|
||||
subtitle.setText(option.summary());
|
||||
subtitle.setVisibility(
|
||||
Strings.isNullOrEmpty(option.summary()) ? View.GONE : View.VISIBLE);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
@@ -16,47 +16,51 @@
|
||||
|
||||
package com.android.settings.notification.modes;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType;
|
||||
import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.OnAddModeListener;
|
||||
import com.android.settings.search.BaseSearchIndexProvider;
|
||||
import com.android.settings.utils.ManagedServiceSettings;
|
||||
import com.android.settings.utils.ZenServiceListing;
|
||||
import com.android.settingslib.core.AbstractPreferenceController;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
import com.android.settingslib.search.SearchIndexable;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
@SearchIndexable
|
||||
public class ZenModesListFragment extends ZenModesFragmentBase {
|
||||
|
||||
private static final ManagedServiceSettings.Config CONFIG = getConditionProviderConfig();
|
||||
static final int REQUEST_NEW_MODE = 101;
|
||||
|
||||
@Nullable private ComponentName mActivityInvokedForAddNew;
|
||||
@Nullable private ImmutableList<String> mZenModeIdsBeforeAddNew;
|
||||
|
||||
@Override
|
||||
protected List<AbstractPreferenceController> createPreferenceControllers(Context context) {
|
||||
ZenServiceListing serviceListing = new ZenServiceListing(getContext(), CONFIG);
|
||||
serviceListing.reloadApprovedServices();
|
||||
return buildPreferenceControllers(context, this, serviceListing);
|
||||
return buildPreferenceControllers(context, this::onAvailableModeTypesForAdd);
|
||||
}
|
||||
|
||||
private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
|
||||
@Nullable Fragment parent, @Nullable ZenServiceListing serviceListing) {
|
||||
OnAddModeListener onAddModeListener) {
|
||||
// We need to redefine ZenModesBackend here even though mBackend exists so that this method
|
||||
// can be static; it must be static to be able to be used in SEARCH_INDEX_DATA_PROVIDER.
|
||||
ZenModesBackend backend = ZenModesBackend.getInstance(context);
|
||||
|
||||
return ImmutableList.of(
|
||||
new ZenModesListPreferenceController(context, parent, backend),
|
||||
new ZenModesListAddModePreferenceController(context, backend, serviceListing)
|
||||
new ZenModesListPreferenceController(context, backend),
|
||||
new ZenModesListAddModePreferenceController(context, onAddModeListener)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,14 +82,55 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
|
||||
return SettingsEnums.NOTIFICATION_ZEN_MODE_AUTOMATION;
|
||||
}
|
||||
|
||||
static ManagedServiceSettings.Config getConditionProviderConfig() {
|
||||
return new ManagedServiceSettings.Config.Builder()
|
||||
.setTag(TAG)
|
||||
.setIntentAction(ConditionProviderService.SERVICE_INTERFACE)
|
||||
.setConfigurationIntentAction(NotificationManager.ACTION_AUTOMATIC_ZEN_RULE)
|
||||
.setPermission(android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE)
|
||||
.setNoun("condition provider")
|
||||
.build();
|
||||
private void onAvailableModeTypesForAdd(List<ModeType> types) {
|
||||
if (types.size() > 1) {
|
||||
// Show dialog to choose the mode to be created. Continue once the user chooses.
|
||||
ZenModesListAddModeTypeChooserDialog.show(this, this::onChosenModeTypeForAdd, types);
|
||||
} else {
|
||||
// Will be custom_manual.
|
||||
onChosenModeTypeForAdd(types.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void onChosenModeTypeForAdd(ModeType type) {
|
||||
if (type.creationActivityIntent() != null) {
|
||||
mActivityInvokedForAddNew = type.creationActivityIntent().getComponent();
|
||||
mZenModeIdsBeforeAddNew = ImmutableList.copyOf(
|
||||
mBackend.getModes().stream().map(ZenMode::getId).toList());
|
||||
startActivityForResult(type.creationActivityIntent(), REQUEST_NEW_MODE);
|
||||
} else {
|
||||
// Custom-manual mode.
|
||||
// TODO: b/326442408 - Transition to the choose-name-and-icon fragment.
|
||||
ZenMode mode = mBackend.addCustomManualMode(
|
||||
"Mode #" + new Random().nextInt(100), 0);
|
||||
if (mode != null) {
|
||||
ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
// If coming back after starting a 3rd-party configuration activity to create a new mode,
|
||||
// try to identify the created mode. Ideally this would be part of the resultCode/data, but
|
||||
// the existing API doesn't work that way...
|
||||
ComponentName activityInvoked = mActivityInvokedForAddNew;
|
||||
ImmutableList<String> previousIds = mZenModeIdsBeforeAddNew;
|
||||
mActivityInvokedForAddNew = null;
|
||||
mZenModeIdsBeforeAddNew = null;
|
||||
if (requestCode != REQUEST_NEW_MODE || previousIds == null || activityInvoked == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we find a new mode owned by the same package, presumably that's it. Open its page.
|
||||
Optional<ZenMode> createdZenMode = mBackend.getModes().stream()
|
||||
.filter(m -> !previousIds.contains(m.getId()))
|
||||
.filter(m -> m.getRule().getPackageName().equals(activityInvoked.getPackageName()))
|
||||
.findFirst();
|
||||
createdZenMode.ifPresent(
|
||||
mode -> ZenSubSettingLauncher.forMode(mContext, mode.getId()).launch());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +151,7 @@ public class ZenModesListFragment extends ZenModesFragmentBase {
|
||||
@Override
|
||||
public List<AbstractPreferenceController> createPreferenceControllers(
|
||||
Context context) {
|
||||
return buildPreferenceControllers(context, null, null);
|
||||
return buildPreferenceControllers(context, ignoredType -> {});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -20,8 +20,6 @@ import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
|
||||
@@ -43,14 +41,10 @@ import java.util.Map;
|
||||
class ZenModesListPreferenceController extends BasePreferenceController {
|
||||
protected static final String KEY = "zen_modes_list";
|
||||
|
||||
@Nullable
|
||||
protected Fragment mParent;
|
||||
protected ZenModesBackend mBackend;
|
||||
|
||||
public ZenModesListPreferenceController(Context context, @Nullable Fragment parent,
|
||||
@NonNull ZenModesBackend backend) {
|
||||
ZenModesListPreferenceController(Context context, @NonNull ZenModesBackend backend) {
|
||||
super(context, KEY);
|
||||
mParent = parent;
|
||||
mBackend = backend;
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Slog;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.settings.utils.ManagedServiceSettings;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class ZenServiceListing {
|
||||
|
||||
static final ManagedServiceSettings.Config CONFIGURATION =
|
||||
new ManagedServiceSettings.Config.Builder()
|
||||
.setTag("ZenServiceListing")
|
||||
.setIntentAction(ConditionProviderService.SERVICE_INTERFACE)
|
||||
.setConfigurationIntentAction(NotificationManager.ACTION_AUTOMATIC_ZEN_RULE)
|
||||
.setPermission(android.Manifest.permission.BIND_CONDITION_PROVIDER_SERVICE)
|
||||
.setNoun("condition provider")
|
||||
.build();
|
||||
|
||||
private final Context mContext;
|
||||
private final Set<ComponentInfo> mApprovedComponents = new ArraySet<>();
|
||||
private final List<Callback> mZenCallbacks = new ArrayList<>();
|
||||
private final NotificationManager mNm;
|
||||
|
||||
ZenServiceListing(Context context) {
|
||||
mContext = context;
|
||||
mNm = context.getSystemService(NotificationManager.class);
|
||||
}
|
||||
|
||||
public ComponentInfo findService(final ComponentName cn) {
|
||||
if (cn == null) {
|
||||
return null;
|
||||
}
|
||||
for (ComponentInfo component : mApprovedComponents) {
|
||||
final ComponentName ci = new ComponentName(component.packageName, component.name);
|
||||
if (ci.equals(cn)) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addZenCallback(Callback callback) {
|
||||
mZenCallbacks.add(callback);
|
||||
}
|
||||
|
||||
public void removeZenCallback(Callback callback) {
|
||||
mZenCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public ImmutableSet<ComponentInfo> loadApprovedComponents() {
|
||||
return loadApprovedComponents(null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public ImmutableSet<ComponentInfo> loadApprovedComponents(@Nullable String restrictToPkg) {
|
||||
mApprovedComponents.clear();
|
||||
|
||||
List<String> enabledNotificationListenerPkgs = mNm.getEnabledNotificationListenerPackages();
|
||||
List<ComponentInfo> components = new ArrayList<>();
|
||||
getServices(CONFIGURATION, components, mContext.getPackageManager(), restrictToPkg);
|
||||
getActivities(CONFIGURATION, components, mContext.getPackageManager(), restrictToPkg);
|
||||
for (ComponentInfo componentInfo : components) {
|
||||
final String pkg = componentInfo.getComponentName().getPackageName();
|
||||
if (mNm.isNotificationPolicyAccessGrantedForPackage(pkg)
|
||||
|| enabledNotificationListenerPkgs.contains(pkg)) {
|
||||
mApprovedComponents.add(componentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mApprovedComponents.isEmpty()) {
|
||||
for (Callback callback : mZenCallbacks) {
|
||||
callback.onComponentsReloaded(mApprovedComponents);
|
||||
}
|
||||
}
|
||||
|
||||
return ImmutableSet.copyOf(mApprovedComponents);
|
||||
}
|
||||
|
||||
private static void getServices(ManagedServiceSettings.Config c, List<ComponentInfo> list,
|
||||
PackageManager pm, @Nullable String restrictToPkg) {
|
||||
final int user = ActivityManager.getCurrentUser();
|
||||
|
||||
Intent queryIntent = new Intent(c.intentAction);
|
||||
if (restrictToPkg != null) {
|
||||
queryIntent.setPackage(restrictToPkg);
|
||||
}
|
||||
List<ResolveInfo> installedServices = pm.queryIntentServicesAsUser(
|
||||
queryIntent,
|
||||
PackageManager.GET_SERVICES | PackageManager.GET_META_DATA,
|
||||
user);
|
||||
|
||||
for (int i = 0, count = installedServices.size(); i < count; i++) {
|
||||
ResolveInfo resolveInfo = installedServices.get(i);
|
||||
ServiceInfo info = resolveInfo.serviceInfo;
|
||||
|
||||
if (!c.permission.equals(info.permission)) {
|
||||
Slog.w(c.tag, "Skipping " + c.noun + " service "
|
||||
+ info.packageName + "/" + info.name
|
||||
+ ": it does not require the permission "
|
||||
+ c.permission);
|
||||
continue;
|
||||
}
|
||||
if (list != null) {
|
||||
list.add(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void getActivities(ManagedServiceSettings.Config c, List<ComponentInfo> list,
|
||||
PackageManager pm, @Nullable String restrictToPkg) {
|
||||
final int user = ActivityManager.getCurrentUser();
|
||||
|
||||
Intent queryIntent = new Intent(c.configIntentAction);
|
||||
if (restrictToPkg != null) {
|
||||
queryIntent.setPackage(restrictToPkg);
|
||||
}
|
||||
List<ResolveInfo> resolveInfos = pm.queryIntentActivitiesAsUser(
|
||||
queryIntent,
|
||||
PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA,
|
||||
user);
|
||||
|
||||
for (int i = 0, count = resolveInfos.size(); i < count; i++) {
|
||||
ResolveInfo resolveInfo = resolveInfos.get(i);
|
||||
ActivityInfo info = resolveInfo.activityInfo;
|
||||
if (list != null) {
|
||||
list.add(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onComponentsReloaded(Set<ComponentInfo> components);
|
||||
}
|
||||
}
|
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID;
|
||||
import static android.service.notification.ConditionProviderService.EXTRA_RULE_ID;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ConfigurationActivityHelperTest {
|
||||
|
||||
private Context mContext;
|
||||
private ConfigurationActivityHelper mHelper;
|
||||
|
||||
@Mock private PackageManager mPm;
|
||||
@Mock private Function<ComponentName, ComponentInfo> mApprovedServiceFinder;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.getApplication();
|
||||
mHelper = new ConfigurationActivityHelper(mPm);
|
||||
|
||||
when(mPm.queryIntentActivities(any(), anyInt())).thenReturn(List.of(new ResolveInfo()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_configActivity() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test"))
|
||||
.build();
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(
|
||||
new ComponentName(mContext.getPackageName(), "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_configActivityNotResolvable_returnsNull()
|
||||
throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test"))
|
||||
.build();
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
when(mPm.queryIntentActivities(any(), anyInt())).thenReturn(new ArrayList<>());
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_configActivityAndWrongPackage_returnsNull()
|
||||
throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setConfigurationActivity(new ComponentName("another", "test"))
|
||||
.build();
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_configActivityAndUnspecifiedOwner()
|
||||
throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(null)
|
||||
.setConfigurationActivity(new ComponentName("another", "test"))
|
||||
.build();
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(new ComponentName("another", "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_cps() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setOwner(new ComponentName(mContext.getPackageName(), "service"))
|
||||
.build();
|
||||
ComponentInfo ci = new ComponentInfo();
|
||||
ci.packageName = mContext.getPackageName();
|
||||
ci.metaData = new Bundle();
|
||||
ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY,
|
||||
ComponentName.flattenToShortString(
|
||||
new ComponentName(mContext.getPackageName(), "activity")));
|
||||
when(mApprovedServiceFinder.apply(new ComponentName(mContext.getPackageName(), "service")))
|
||||
.thenReturn(ci);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(
|
||||
new ComponentName(mContext.getPackageName(), "activity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getConfigurationActivityIntentForMode_cpsAndWrongPackage_returnsNull()
|
||||
throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage("other")
|
||||
.setOwner(new ComponentName(mContext.getPackageName(), "service"))
|
||||
.build();
|
||||
ComponentInfo ci = new ComponentInfo();
|
||||
ci.packageName = mContext.getPackageName();
|
||||
ci.metaData = new Bundle();
|
||||
ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY,
|
||||
ComponentName.flattenToShortString(
|
||||
new ComponentName(mContext.getPackageName(), "activity")));
|
||||
when(mApprovedServiceFinder.apply(new ComponentName(mContext.getPackageName(), "service")))
|
||||
.thenReturn(ci);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mHelper.getConfigurationActivityIntentForMode(mode, mApprovedServiceFinder);
|
||||
|
||||
assertThat(res).isNull();
|
||||
}
|
||||
}
|
@@ -84,6 +84,18 @@ class TestModeBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
TestModeBuilder setOwner(ComponentName owner) {
|
||||
mRule.setOwner(owner);
|
||||
mConfigZenRule.component = owner;
|
||||
return this;
|
||||
}
|
||||
|
||||
TestModeBuilder setConfigurationActivity(ComponentName configActivity) {
|
||||
mRule.setConfigurationActivity(configActivity);
|
||||
mConfigZenRule.configurationActivity = configActivity;
|
||||
return this;
|
||||
}
|
||||
|
||||
TestModeBuilder setConditionId(Uri conditionId) {
|
||||
mRule.setConditionId(conditionId);
|
||||
mConfigZenRule.conditionId = conditionId;
|
||||
@@ -150,18 +162,6 @@ class TestModeBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
TestModeBuilder setConfigurationActivity(ComponentName configActivity) {
|
||||
mRule.setConfigurationActivity(configActivity);
|
||||
mConfigZenRule.configurationActivity = configActivity;
|
||||
return this;
|
||||
}
|
||||
|
||||
TestModeBuilder setOwner(ComponentName owner) {
|
||||
mRule.setOwner(owner);
|
||||
mConfigZenRule.component = owner;
|
||||
return this;
|
||||
}
|
||||
|
||||
ZenMode build() {
|
||||
return new ZenMode(mId, mRule, mConfigZenRule);
|
||||
}
|
||||
|
@@ -19,30 +19,26 @@ package com.android.settings.notification.modes;
|
||||
import static android.app.AutomaticZenRule.TYPE_OTHER;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
|
||||
import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME;
|
||||
import static android.app.NotificationManager.EXTRA_AUTOMATIC_RULE_ID;
|
||||
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
|
||||
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
|
||||
import static android.service.notification.ConditionProviderService.EXTRA_RULE_ID;
|
||||
|
||||
import static com.android.settings.notification.modes.ZenModeSetTriggerLinkPreferenceController.AUTOMATIC_TRIGGER_PREF_KEY;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.AutomaticZenRule;
|
||||
import android.app.Flags;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.platform.test.annotations.EnableFlags;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.service.notification.ConditionProviderService;
|
||||
import android.service.notification.SystemZenRules;
|
||||
import android.service.notification.ZenModeConfig;
|
||||
|
||||
@@ -52,7 +48,6 @@ import androidx.test.core.app.ApplicationProvider;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.SettingsActivity;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
import com.android.settings.utils.ZenServiceListing;
|
||||
import com.android.settingslib.PrimarySwitchPreference;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
@@ -80,10 +75,10 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
|
||||
private PrimarySwitchPreference mPreference;
|
||||
|
||||
@Mock
|
||||
private ZenServiceListing mServiceListing;
|
||||
@Mock
|
||||
private PackageManager mPm;
|
||||
@Mock
|
||||
private ConfigurationActivityHelper mConfigurationActivityHelper;
|
||||
|
||||
@Mock
|
||||
private PreferenceCategory mPrefCategory;
|
||||
@@ -98,8 +93,9 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
mContext = ApplicationProvider.getApplicationContext();
|
||||
|
||||
mPrefController = new ZenModeSetTriggerLinkPreferenceController(mContext,
|
||||
"zen_automatic_trigger_category", mFragment, mBackend, mPm);
|
||||
mPrefController.setServiceListing(mServiceListing);
|
||||
"zen_automatic_trigger_category", mFragment, mBackend,
|
||||
mConfigurationActivityHelper,
|
||||
mock(ZenServiceListing.class));
|
||||
mPreference = new PrimarySwitchPreference(mContext);
|
||||
|
||||
when(mPrefCategory.findPreference(AUTOMATIC_TRIGGER_PREF_KEY)).thenReturn(mPreference);
|
||||
@@ -225,6 +221,40 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
assertThat(mPreference.getOnPreferenceClickListener()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuleLink_appWithConfigActivity_linksToConfigActivity() {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage("some.package")
|
||||
.setTriggerDescription("When The Music's Over")
|
||||
.build();
|
||||
Intent configurationIntent = new Intent("configure the mode");
|
||||
when(mConfigurationActivityHelper.getConfigurationActivityIntentForMode(any(), any()))
|
||||
.thenReturn(configurationIntent);
|
||||
|
||||
mPrefController.updateZenMode(mPrefCategory, mode);
|
||||
|
||||
assertThat(mPreference.getTitle()).isNotNull();
|
||||
assertThat(mPreference.getTitle().toString()).isEqualTo(
|
||||
mContext.getString(R.string.zen_mode_configuration_link_title));
|
||||
assertThat(mPreference.getSummary()).isNotNull();
|
||||
assertThat(mPreference.getSummary().toString()).isEqualTo("When The Music's Over");
|
||||
assertThat(mPreference.getIntent()).isEqualTo(configurationIntent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRuleLink_appWithoutConfigActivity_hidden() {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage("some.package")
|
||||
.setTriggerDescription("Will not be shown :(")
|
||||
.build();
|
||||
when(mConfigurationActivityHelper.getConfigurationActivityIntentForMode(any(), any()))
|
||||
.thenReturn(null);
|
||||
|
||||
mPrefController.updateZenMode(mPrefCategory, mode);
|
||||
|
||||
assertThat(mPrefCategory.isVisible()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onScheduleChosen_updatesMode() {
|
||||
ZenMode originalMode = new TestModeBuilder()
|
||||
@@ -253,109 +283,4 @@ public class ZenModeSetTriggerLinkPreferenceControllerTest {
|
||||
assertThat(updatedMode.getRule().getOwner()).isEqualTo(
|
||||
ZenModeConfig.getScheduleConditionProvider());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAppRuleIntent_configActivity() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setConfigurationActivity(new ComponentName(mContext.getPackageName(), "test"))
|
||||
.setType(TYPE_OTHER)
|
||||
.setTriggerDescription("some rule")
|
||||
.build();
|
||||
|
||||
when(mPm.getPackageUid(null, 0)).thenReturn(-1);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mPrefController.getAppRuleIntent(mode);
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(
|
||||
new ComponentName(mContext.getPackageName(), "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAppRuleIntent_configActivity_wrongPackage() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setConfigurationActivity(new ComponentName("another", "test"))
|
||||
.setType(TYPE_OTHER)
|
||||
.build();
|
||||
|
||||
when(mPm.getPackageUid(null, 0)).thenReturn(-1);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mPrefController.getAppRuleIntent(mode);
|
||||
assertThat(res).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAppRuleIntent_configActivity_unspecifiedOwner() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(null)
|
||||
.setConfigurationActivity(new ComponentName("another", "test"))
|
||||
.setType(TYPE_OTHER)
|
||||
.build();
|
||||
|
||||
when(mPm.getPackageUid(null, 0)).thenReturn(-1);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mPrefController.getAppRuleIntent(mode);
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(new ComponentName("another", "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAppRuleIntent_cps() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setId("id")
|
||||
.setPackage(mContext.getPackageName())
|
||||
.setOwner(new ComponentName(mContext.getPackageName(), "service"))
|
||||
.build();
|
||||
|
||||
ComponentInfo ci = new ComponentInfo();
|
||||
ci.packageName = mContext.getPackageName();
|
||||
ci.metaData = new Bundle();
|
||||
ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY,
|
||||
ComponentName.flattenToShortString(
|
||||
new ComponentName(mContext.getPackageName(), "activity")));
|
||||
|
||||
when(mServiceListing.findService(new ComponentName(mContext.getPackageName(), "service")))
|
||||
.thenReturn(ci);
|
||||
when(mPm.getPackageUid(null, 0)).thenReturn(-1);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mPrefController.getAppRuleIntent(mode);
|
||||
assertThat(res).isNotNull();
|
||||
assertThat(res.getStringExtra(EXTRA_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getStringExtra(EXTRA_AUTOMATIC_RULE_ID)).isEqualTo("id");
|
||||
assertThat(res.getComponent()).isEqualTo(
|
||||
new ComponentName(mContext.getPackageName(), "activity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAppRuleIntent_cps_wrongPackage() throws Exception {
|
||||
ZenMode mode = new TestModeBuilder()
|
||||
.setPackage("other")
|
||||
.setOwner(new ComponentName(mContext.getPackageName(), "service"))
|
||||
.setType(TYPE_OTHER)
|
||||
.build();
|
||||
|
||||
ComponentInfo ci = new ComponentInfo();
|
||||
ci.packageName = mContext.getPackageName();
|
||||
ci.metaData = new Bundle();
|
||||
ci.metaData.putString(ConditionProviderService.META_DATA_CONFIGURATION_ACTIVITY,
|
||||
ComponentName.flattenToShortString(
|
||||
new ComponentName(mContext.getPackageName(), "activity")));
|
||||
|
||||
when(mPm.getPackageUid(null, 0)).thenReturn(-1);
|
||||
when(mPm.getPackageUid(mContext.getPackageName(), 0)).thenReturn(1);
|
||||
|
||||
Intent res = mPrefController.getAppRuleIntent(mode);
|
||||
assertThat(res).isNull();
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import static android.app.NotificationManager.META_DATA_AUTOMATIC_RULE_TYPE;
|
||||
import static android.app.NotificationManager.META_DATA_RULE_INSTANCE_LIMIT;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.ComponentInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.mockito.stubbing.Answer;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModesListAddModePreferenceControllerTest {
|
||||
|
||||
private Context mContext;
|
||||
private ZenModesListAddModePreferenceController mController;
|
||||
|
||||
@Mock private ZenModesListAddModePreferenceController.OnAddModeListener mListener;
|
||||
@Mock private ZenServiceListing mZenServiceListing;
|
||||
@Mock private ConfigurationActivityHelper mConfigurationActivityHelper;
|
||||
@Mock private NotificationManager mNm;
|
||||
@Mock private PackageManager mPm;
|
||||
|
||||
@Captor private ArgumentCaptor<List<ModeType>> mListenerCaptor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.getApplication();
|
||||
Function<ApplicationInfo, Drawable> appIconRetriever = appInfo -> new ColorDrawable();
|
||||
|
||||
mController = new ZenModesListAddModePreferenceController(mContext, mListener,
|
||||
mZenServiceListing, mConfigurationActivityHelper, mNm, mPm, appIconRetriever,
|
||||
MoreExecutors.newDirectExecutorService(), MoreExecutors.directExecutor());
|
||||
|
||||
when(mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(any()))
|
||||
.thenAnswer((Answer<ComponentName>) invocationOnMock -> {
|
||||
// By default, assume the ComponentInfo is also the configurationActivity.
|
||||
ComponentInfo ci = invocationOnMock.getArgument(0);
|
||||
return ci != null ? ci.getComponentName() : null;
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_noAppProviders_onlyOptionIsCustom() {
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(ImmutableSet.of());
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(1);
|
||||
assertThat(options.get(0).name()).isEqualTo("Custom");
|
||||
assertThat(options.get(0).summary()).isNull();
|
||||
assertThat(options.get(0).icon()).isNotNull();
|
||||
assertThat(options.get(0).creationActivityIntent()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_someAppProviders_includedInOptions() {
|
||||
ImmutableSet<ComponentInfo> approvedComponents = ImmutableSet.of(
|
||||
newComponentInfoWithValidMetadata("pkg1"),
|
||||
newComponentInfoWithValidMetadata("pkg2"));
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents);
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(3);
|
||||
|
||||
assertThat(options.get(1).name()).isEqualTo("Rule by pkg1");
|
||||
assertThat(options.get(1).summary()).isEqualTo("A package called pkg1");
|
||||
assertThat(options.get(1).icon()).isNotNull();
|
||||
assertThat(options.get(1).creationActivityIntent()).isNotNull();
|
||||
assertThat(options.get(1).creationActivityIntent().getComponent()).isEqualTo(
|
||||
new ComponentName("pkg1", "pkg1.activity"));
|
||||
|
||||
assertThat(options.get(0).name()).isEqualTo("Custom");
|
||||
assertThat(options.get(2).name()).isEqualTo("Rule by pkg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_someAppProviders_optionsAreSorted() {
|
||||
ImmutableSet<ComponentInfo> approvedComponents = ImmutableSet.of(
|
||||
newComponentInfoWithValidMetadata("pkg_Z"),
|
||||
newComponentInfoWithValidMetadata("pkg_A"),
|
||||
newComponentInfoWithValidMetadata("pkg_F"),
|
||||
newComponentInfoWithValidMetadata("pkg_C"));
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents);
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(5);
|
||||
assertThat(options.stream().map(o -> o.name()).toList())
|
||||
.containsExactly("Custom", "Rule by pkg_A", "Rule by pkg_C", "Rule by pkg_F",
|
||||
"Rule by pkg_Z")
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_appProviderWithMissingMetadata_notAnOption() {
|
||||
ComponentInfo componentWithoutRuleType = newComponentInfoWithValidMetadata("pkg1");
|
||||
componentWithoutRuleType.metaData.remove(META_DATA_AUTOMATIC_RULE_TYPE);
|
||||
ImmutableSet<ComponentInfo> approvedComponents = ImmutableSet.of(
|
||||
componentWithoutRuleType, newComponentInfoWithValidMetadata("pkg2"));
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents);
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(2);
|
||||
assertThat(options.get(0).name()).isEqualTo("Custom");
|
||||
assertThat(options.get(1).name()).isEqualTo("Rule by pkg2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_appProviderWithRuleLimitExceeded_notAnOption() {
|
||||
ComponentInfo componentWithLimitThreeRules = newComponentInfoWithValidMetadata("pkg1");
|
||||
componentWithLimitThreeRules.metaData.putInt(META_DATA_RULE_INSTANCE_LIMIT, 3);
|
||||
ImmutableSet<ComponentInfo> approvedComponents = ImmutableSet.of(
|
||||
componentWithLimitThreeRules, newComponentInfoWithValidMetadata("pkg2"));
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents);
|
||||
when(mNm.getRuleInstanceCount(any())).thenReturn(3); // Already 3 created rules.
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(2);
|
||||
assertThat(options.get(0).name()).isEqualTo("Custom");
|
||||
assertThat(options.get(1).name()).isEqualTo("Rule by pkg2");
|
||||
verify(mNm).getRuleInstanceCount(eq(componentWithLimitThreeRules.getComponentName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onClickAddMode_appProviderWithoutConfigurationActivity_notAnOption() {
|
||||
ComponentInfo componentWithoutConfigActivity = newComponentInfoWithValidMetadata("pkg2");
|
||||
ImmutableSet<ComponentInfo> approvedComponents = ImmutableSet.of(
|
||||
newComponentInfoWithValidMetadata("pkg1"), componentWithoutConfigActivity);
|
||||
when(mZenServiceListing.loadApprovedComponents()).thenReturn(approvedComponents);
|
||||
when(mConfigurationActivityHelper.getConfigurationActivityFromApprovedComponent(any()))
|
||||
.thenAnswer((Answer<ComponentName>) invocationOnMock -> {
|
||||
ComponentInfo ci = invocationOnMock.getArgument(0);
|
||||
if (ci == componentWithoutConfigActivity) {
|
||||
return null;
|
||||
} else {
|
||||
return ci.getComponentName();
|
||||
}
|
||||
});
|
||||
|
||||
mController.onClickAddMode();
|
||||
|
||||
verify(mListener).onAvailableModeTypesForAdd(mListenerCaptor.capture());
|
||||
List<ModeType> options = mListenerCaptor.getValue();
|
||||
assertThat(options).hasSize(2);
|
||||
assertThat(options.get(0).name()).isEqualTo("Custom");
|
||||
assertThat(options.get(1).name()).isEqualTo("Rule by pkg1");
|
||||
}
|
||||
|
||||
private ComponentInfo newComponentInfoWithValidMetadata(String pkg) {
|
||||
ComponentInfo ci = new ActivityInfo();
|
||||
|
||||
ci.applicationInfo = mock(ApplicationInfo.class);
|
||||
when(ci.applicationInfo.loadLabel(any())).thenReturn("A package called " + pkg);
|
||||
when(ci.applicationInfo.loadUnbadgedIcon(any())).thenReturn(new ColorDrawable());
|
||||
ci.packageName = pkg;
|
||||
ci.name = pkg + ".activity";
|
||||
ci.metaData = new Bundle();
|
||||
ci.metaData.putString(META_DATA_AUTOMATIC_RULE_TYPE, "Rule by " + pkg);
|
||||
|
||||
return ci;
|
||||
}
|
||||
}
|
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.notification.modes;
|
||||
|
||||
import static android.provider.Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID;
|
||||
|
||||
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT;
|
||||
import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS;
|
||||
import static com.android.settings.notification.modes.ZenModesListFragment.REQUEST_NEW_MODE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.testing.EmptyFragmentActivity;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
|
||||
import com.android.settings.notification.modes.ZenModesListAddModePreferenceController.ModeType;
|
||||
import com.android.settingslib.notification.modes.ZenMode;
|
||||
import com.android.settingslib.notification.modes.ZenModesBackend;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.shadows.ShadowActivity.IntentForResult;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ZenModesListFragmentTest {
|
||||
|
||||
private static final ModeType APP_PROVIDED_MODE_TYPE = new ModeType("Mode", new ColorDrawable(),
|
||||
"Details", new Intent().setComponent(new ComponentName("pkg", "configActivity")));
|
||||
|
||||
private static final ImmutableList<ZenMode> EXISTING_MODES = ImmutableList.of(
|
||||
new TestModeBuilder().setId("A").build(),
|
||||
new TestModeBuilder().setId("B").build(),
|
||||
new TestModeBuilder().setId("C").build());
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<EmptyFragmentActivity> mActivityScenario =
|
||||
new ActivityScenarioRule<>(EmptyFragmentActivity.class);
|
||||
|
||||
private FragmentActivity mActivity;
|
||||
private ZenModesListFragment mFragment;
|
||||
@Mock private ZenModesBackend mBackend;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mFragment = new ZenModesListFragment();
|
||||
mActivityScenario.getScenario().onActivity(activity -> {
|
||||
activity.getSupportFragmentManager().beginTransaction()
|
||||
.add(mFragment, "tag").commitNow();
|
||||
mActivity = activity;
|
||||
});
|
||||
|
||||
mFragment.setBackend(mBackend); // after onAttach()
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onChosenModeTypeForAdd_appProvidedMode_startsCreationActivity() {
|
||||
when(mBackend.getModes()).thenReturn(EXISTING_MODES);
|
||||
|
||||
mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE);
|
||||
|
||||
IntentForResult intent = shadowOf(mActivity).getNextStartedActivityForResult();
|
||||
assertThat(intent).isNotNull();
|
||||
assertThat(intent.intent).isEqualTo(APP_PROVIDED_MODE_TYPE.creationActivityIntent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityResult_modeWasCreated_opensIt() {
|
||||
when(mBackend.getModes()).thenReturn(EXISTING_MODES);
|
||||
mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE);
|
||||
|
||||
// App creates the new mode.
|
||||
ZenMode createdMode = new TestModeBuilder().setId("new_id").setPackage("pkg").build();
|
||||
when(mBackend.getModes()).thenReturn(new ImmutableList.Builder<ZenMode>()
|
||||
.addAll(EXISTING_MODES)
|
||||
.add(createdMode)
|
||||
.build());
|
||||
mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent());
|
||||
|
||||
Intent openModePageIntent = shadowOf(mActivity).getNextStartedActivity();
|
||||
assertThat(openModePageIntent.getStringExtra(EXTRA_SHOW_FRAGMENT))
|
||||
.isEqualTo(ZenModeFragment.class.getName());
|
||||
Bundle fragmentArgs = openModePageIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
|
||||
assertThat(fragmentArgs).isNotNull();
|
||||
assertThat(fragmentArgs.getString(EXTRA_AUTOMATIC_ZEN_RULE_ID)).isEqualTo("new_id");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityResult_secondTime_doesNothing() {
|
||||
when(mBackend.getModes()).thenReturn(EXISTING_MODES);
|
||||
mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE);
|
||||
// App creates a new mode, we redirect to its page when coming back.
|
||||
ZenMode createdMode = new TestModeBuilder().setId("new_id").setPackage("pkg").build();
|
||||
when(mBackend.getModes()).thenReturn(new ImmutableList.Builder<ZenMode>()
|
||||
.addAll(EXISTING_MODES)
|
||||
.add(createdMode)
|
||||
.build());
|
||||
mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent());
|
||||
shadowOf(mActivity).clearNextStartedActivities();
|
||||
|
||||
mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent());
|
||||
|
||||
Intent nextIntent = shadowOf(mActivity).getNextStartedActivity();
|
||||
assertThat(nextIntent).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onActivityResult_modeWasNotCreated_doesNothing() {
|
||||
when(mBackend.getModes()).thenReturn(EXISTING_MODES);
|
||||
mFragment.onChosenModeTypeForAdd(APP_PROVIDED_MODE_TYPE);
|
||||
shadowOf(mActivity).clearNextStartedActivities();
|
||||
|
||||
// Returning to settings without creating a new mode.
|
||||
mFragment.onActivityResult(REQUEST_NEW_MODE, 0, new Intent());
|
||||
|
||||
Intent nextIntent = shadowOf(mActivity).getNextStartedActivity();
|
||||
assertThat(nextIntent).isNull();
|
||||
}
|
||||
}
|
@@ -99,7 +99,7 @@ public class ZenModesListPreferenceControllerTest {
|
||||
PreferenceScreen preferenceScreen = preferenceManager.createPreferenceScreen(mContext);
|
||||
preferenceScreen.addPreference(mPreference);
|
||||
|
||||
mPrefController = new ZenModesListPreferenceController(mContext, null, mBackend);
|
||||
mPrefController = new ZenModesListPreferenceController(mContext, mBackend);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
Reference in New Issue
Block a user