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

Commit 256b88d8 authored by Ioana Alexandru's avatar Ioana Alexandru Committed by Android (Google) Code Review
Browse files

Merge "Move ZenModesBackend to SettingsLib." into main

parents 88b6b4d7 d05649e9
Loading
Loading
Loading
Loading
+28 −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:viewportWidth="24.0"
        android:viewportHeight="24.0"
        android:tint="?android:attr/colorControlNormal">
    <path
        android:fillColor="#FFFFFFFF"
        android:pathData="M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8c0,-4.41 3.59,-8 8,-8c4.41,0 8,3.59 8,8C20,16.41 16.41,20 12,20z"/>
    <path
        android:fillColor="#FFFFFFFF"
        android:pathData="M7,11h10v2h-10z"/>
</vector>
+6 −0
Original line number Diff line number Diff line
@@ -1361,6 +1361,9 @@
    <!-- Keywords for setting screen for controlling apps that can schedule alarms [CHAR LIMIT=100] -->
    <string name="keywords_alarms_and_reminders">schedule, alarm, reminder, clock</string>

    <!-- Sound: Title for the Do not Disturb option and associated settings page. [CHAR LIMIT=50]-->
    <string name="zen_mode_settings_title">Do Not Disturb</string>

    <!--  Do not disturb: Label for button in enable zen dialog that will turn on zen mode. [CHAR LIMIT=30] -->
    <string name="zen_mode_enable_dialog_turn_on">Turn on</string>
    <!-- Do not disturb: Title for the Do not Disturb dialog to turn on Do not disturb. [CHAR LIMIT=50]-->
@@ -1387,6 +1390,9 @@
    <!-- Do not disturb: Duration option to always have DND on until it is manually turned off [CHAR LIMIT=60] -->
    <string name="zen_mode_forever">Until you turn off</string>

    <!-- [CHAR LIMIT=50] Zen mode settings: placeholder for a Contact name when the name is empty -->
    <string name="zen_mode_starred_contacts_empty_name">(No name)</string>

    <!-- time label for event have that happened very recently [CHAR LIMIT=60] -->
    <string name="time_unit_just_now">Just now</string>

+160 −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.settingslib.notification.modes;

import static com.google.common.util.concurrent.Futures.immediateFuture;

import static java.util.Objects.requireNonNull;

import android.annotation.Nullable;
import android.app.AutomaticZenRule;
import android.content.Context;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.service.notification.SystemZenRules;
import android.text.TextUtils;
import android.util.Log;
import android.util.LruCache;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;

import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ZenIconLoader {

    private static final String TAG = "ZenIconLoader";

    private static final Drawable MISSING = new ColorDrawable();

    @Nullable // Until first usage
    private static ZenIconLoader sInstance;

    private final LruCache<String, Drawable> mCache;
    private final ListeningExecutorService mBackgroundExecutor;

    public static ZenIconLoader getInstance() {
        if (sInstance == null) {
            sInstance = new ZenIconLoader();
        }
        return sInstance;
    }

    private ZenIconLoader() {
        this(Executors.newFixedThreadPool(4));
    }

    @VisibleForTesting
    ZenIconLoader(ExecutorService backgroundExecutor) {
        mCache = new LruCache<>(50);
        mBackgroundExecutor =
                MoreExecutors.listeningDecorator(backgroundExecutor);
    }

    @NonNull
    ListenableFuture<Drawable> getIcon(Context context, @NonNull AutomaticZenRule rule) {
        if (rule.getIconResId() == 0) {
            return Futures.immediateFuture(getFallbackIcon(context, rule.getType()));
        }

        return FluentFuture.from(loadIcon(context, rule.getPackageName(), rule.getIconResId()))
                .transform(icon ->
                                icon != null ? icon : getFallbackIcon(context, rule.getType()),
                        MoreExecutors.directExecutor());
    }

    @NonNull
    private ListenableFuture</* @Nullable */ Drawable> loadIcon(Context context, String pkg,
            int iconResId) {
        String cacheKey = pkg + ":" + iconResId;
        synchronized (mCache) {
            Drawable cachedValue = mCache.get(cacheKey);
            if (cachedValue != null) {
                return immediateFuture(cachedValue != MISSING ? cachedValue : null);
            }
        }

        return FluentFuture.from(mBackgroundExecutor.submit(() -> {
            if (TextUtils.isEmpty(pkg) || SystemZenRules.PACKAGE_ANDROID.equals(pkg)) {
                return context.getDrawable(iconResId);
            } else {
                Context appContext = context.createPackageContext(pkg, 0);
                Drawable appDrawable = AppCompatResources.getDrawable(appContext, iconResId);
                return getMonochromeIconIfPresent(appDrawable);
            }
        })).catching(Exception.class, ex -> {
            // If we cannot resolve the icon, then store MISSING in the cache below, so
            // we don't try again.
            Log.e(TAG, "Error while loading icon " + cacheKey, ex);
            return null;
        }, MoreExecutors.directExecutor()).transform(drawable -> {
            synchronized (mCache) {
                mCache.put(cacheKey, drawable != null ? drawable : MISSING);
            }
            return drawable;
        }, MoreExecutors.directExecutor());
    }

    private static Drawable getFallbackIcon(Context context, int ruleType) {
        int iconResIdFromType = switch (ruleType) {
            case AutomaticZenRule.TYPE_UNKNOWN ->
                    com.android.internal.R.drawable.ic_zen_mode_type_unknown;
            case AutomaticZenRule.TYPE_OTHER ->
                    com.android.internal.R.drawable.ic_zen_mode_type_other;
            case AutomaticZenRule.TYPE_SCHEDULE_TIME ->
                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_time;
            case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR ->
                    com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar;
            case AutomaticZenRule.TYPE_BEDTIME ->
                    com.android.internal.R.drawable.ic_zen_mode_type_bedtime;
            case AutomaticZenRule.TYPE_DRIVING ->
                    com.android.internal.R.drawable.ic_zen_mode_type_driving;
            case AutomaticZenRule.TYPE_IMMERSIVE ->
                    com.android.internal.R.drawable.ic_zen_mode_type_immersive;
            case AutomaticZenRule.TYPE_THEATER ->
                    com.android.internal.R.drawable.ic_zen_mode_type_theater;
            case AutomaticZenRule.TYPE_MANAGED ->
                    com.android.internal.R.drawable.ic_zen_mode_type_managed;
            default -> com.android.internal.R.drawable.ic_zen_mode_type_unknown;
        };
        return requireNonNull(context.getDrawable(iconResIdFromType));
    }

    private static Drawable getMonochromeIconIfPresent(Drawable icon) {
        // For created rules, the app should've provided a monochrome Drawable. However, implicit
        // rules have the app's icon, which is not -- but might have a monochrome layer. Thus
        // we choose it, if present.
        if (icon instanceof AdaptiveIconDrawable adaptiveIcon) {
            if (adaptiveIcon.getMonochrome() != null) {
                // Wrap with negative inset => scale icon (inspired from BaseIconFactory)
                return new InsetDrawable(adaptiveIcon.getMonochrome(),
                        -2.0f * AdaptiveIconDrawable.getExtraInsetFraction());
            }
        }
        return icon;
    }
}
+257 −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.settingslib.notification.modes;

import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;

import static com.google.common.base.Preconditions.checkState;

import static java.util.Objects.requireNonNull;

import android.annotation.SuppressLint;
import android.app.AutomaticZenRule;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.service.notification.SystemZenRules;
import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.settingslib.R;

import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.Objects;

/**
 * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
 *
 * <p>It also adapts other rule features that we don't want to expose in the UI, such as
 * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
 */
public class ZenMode {

    private static final String TAG = "ZenMode";

    public static final String MANUAL_DND_MODE_ID = "manual_dnd";

    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
            new ZenPolicy.Builder()
                    .disallowAllSounds()
                    .allowAlarms(true)
                    .allowMedia(true)
                    .allowPriorityChannels(false)
                    .build();

    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
            new ZenPolicy.Builder()
                    .disallowAllSounds()
                    .hideAllVisualEffects()
                    .allowPriorityChannels(false)
                    .build();

    private final String mId;
    private AutomaticZenRule mRule;
    private final boolean mIsActive;
    private final boolean mIsManualDnd;

    public ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
        this(id, rule, isActive, false);
    }

    private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
        mId = id;
        mRule = rule;
        mIsActive = isActive;
        mIsManualDnd = isManualDnd;
    }

    public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
        return new ZenMode(MANUAL_DND_MODE_ID, manualRule, isActive, true);
    }

    @NonNull
    public String getId() {
        return mId;
    }

    @NonNull
    public AutomaticZenRule getRule() {
        return mRule;
    }

    @NonNull
    public ListenableFuture<Drawable> getIcon(@NonNull Context context,
            @NonNull ZenIconLoader iconLoader) {
        if (mIsManualDnd) {
            return Futures.immediateFuture(requireNonNull(
                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
        }

        return iconLoader.getIcon(context, mRule);
    }

    @NonNull
    public ZenPolicy getPolicy() {
        switch (mRule.getInterruptionFilter()) {
            case INTERRUPTION_FILTER_PRIORITY:
            case NotificationManager.INTERRUPTION_FILTER_ALL:
                return requireNonNull(mRule.getZenPolicy());

            case NotificationManager.INTERRUPTION_FILTER_ALARMS:
                return POLICY_INTERRUPTION_FILTER_ALARMS;

            case NotificationManager.INTERRUPTION_FILTER_NONE:
                return POLICY_INTERRUPTION_FILTER_NONE;

            case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
            default:
                Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
                        + mRule.getInterruptionFilter());
                return requireNonNull(mRule.getZenPolicy());
        }
    }

    /**
     * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
     * supplied policy. In some cases this involves conversions, so that the following call
     * to {@link #getPolicy} might return a different policy from the one supplied here.
     */
    @SuppressLint("WrongConstant")
    public void setPolicy(@NonNull ZenPolicy policy) {
        ZenPolicy currentPolicy = getPolicy();
        if (currentPolicy.equals(policy)) {
            return;
        }

        if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) {
            Log.wtf(TAG, "Able to change policy without filtering being enabled");
        }

        // If policy is customized from any of the "special" ones, make the rule PRIORITY.
        if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
            mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
        }
        mRule.setZenPolicy(policy);
    }

    @NonNull
    public ZenDeviceEffects getDeviceEffects() {
        return mRule.getDeviceEffects() != null
                ? mRule.getDeviceEffects()
                : new ZenDeviceEffects.Builder().build();
    }

    public void setCustomModeConditionId(Context context, Uri conditionId) {
        checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()),
                "Trying to change condition of non-system-owned rule %s (to %s)",
                mRule, conditionId);

        Uri oldCondition = mRule.getConditionId();
        mRule.setConditionId(conditionId);

        ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId);
        if (scheduleInfo != null) {
            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME);
            mRule.setOwner(ZenModeConfig.getScheduleConditionProvider());
            mRule.setTriggerDescription(
                    getTriggerDescriptionForScheduleTime(context, scheduleInfo));
            return;
        }

        ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId);
        if (eventInfo != null) {
            mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
            mRule.setOwner(ZenModeConfig.getEventConditionProvider());
            mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo));
            return;
        }

        if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) {
            mRule.setType(AutomaticZenRule.TYPE_OTHER);
            mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider());
            mRule.setTriggerDescription("");
            return;
        }

        Log.wtf(TAG, String.format(
                "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of "
                        + "condition it was!",
                mRule, oldCondition, conditionId));
    }

    public boolean canEditName() {
        return !isManualDnd();
    }

    public boolean canEditIcon() {
        return !isManualDnd();
    }

    public boolean canBeDeleted() {
        return !isManualDnd();
    }

    public boolean isManualDnd() {
        return mIsManualDnd;
    }

    public boolean isActive() {
        return mIsActive;
    }

    public boolean isSystemOwned() {
        return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName());
    }

    @AutomaticZenRule.Type
    public int getType() {
        return mRule.getType();
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        return obj instanceof ZenMode other
                && mId.equals(other.mId)
                && mRule.equals(other.mRule)
                && mIsActive == other.mIsActive;
    }

    @Override
    public int hashCode() {
        return Objects.hash(mId, mRule, mIsActive);
    }

    @Override
    public String toString() {
        return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
    }
}
+207 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading