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:
Matías Hernández
2024-06-27 19:18:06 +02:00
parent d26521f7bb
commit 2639c19474
17 changed files with 1300 additions and 266 deletions

View File

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

View File

@@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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