Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2639c194 authored by Matías Hernández's avatar Matías Hernández
Browse files

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
parent d26521f7
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
<!--
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>
 No newline at end of file
+6 −0
Original line number Diff line number Diff line
@@ -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>
+143 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+1 −2
Original line number Diff line number Diff line
@@ -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(
+19 −94
Original line number Diff line number Diff line
@@ -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;
    }
}
Loading