blob: 87158cd6fe293fb3ca065ba028eb0dfd6fed51be [file] [log] [blame]
/*
* Copyright (C) 2023 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.server.notification;
import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_AUTOMATIC;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_MANUAL;
import static android.service.notification.NotificationServiceProto.RULE_TYPE_UNKNOWN;
import android.annotation.NonNull;
import android.app.Flags;
import android.app.NotificationManager;
import android.content.pm.PackageManager;
import android.os.Process;
import android.service.notification.DNDPolicyProto;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeDiff;
import android.service.notification.ZenPolicy;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.util.FrameworkStatsLog;
import java.io.ByteArrayOutputStream;
import java.util.Objects;
/**
* Class for writing DNDStateChanged atoms to the statsd log.
* Use ZenModeEventLoggerFake for testing.
*/
class ZenModeEventLogger {
private static final String TAG = "ZenModeEventLogger";
// Placeholder int for unknown zen mode, to distinguish from "off".
static final int ZEN_MODE_UNKNOWN = -1;
// Object for tracking config changes and policy changes associated with an overall zen
// mode change.
ZenModeEventLogger.ZenStateChanges mChangeState = new ZenModeEventLogger.ZenStateChanges();
private PackageManager mPm;
ZenModeEventLogger(PackageManager pm) {
mPm = pm;
}
/**
* Enum used to log the type of DND state changed events.
* These use UiEvent IDs for ease of integrating with other UiEvents.
*/
enum ZenStateChangedEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "DND was turned on; may additionally include policy change.")
DND_TURNED_ON(1368),
@UiEvent(doc = "DND was turned off; may additionally include policy change.")
DND_TURNED_OFF(1369),
@UiEvent(doc = "DND policy was changed but the zen mode did not change.")
DND_POLICY_CHANGED(1370),
@UiEvent(doc = "Change in DND automatic rules active, without changing mode or policy.")
DND_ACTIVE_RULES_CHANGED(1371);
private final int mId;
ZenStateChangedEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
}
/**
* Potentially log a zen mode change if the provided config and policy changes warrant it.
*
* @param prevInfo ZenModeInfo (zen mode setting, config, policy) prior to this change
* @param newInfo ZenModeInfo after this change takes effect
* @param callingUid the calling UID associated with the change; may be used to attribute the
* change to a particular package or determine if this is a user action
* @param fromSystemOrSystemUi whether the calling UID is either system UID or system UI
*/
public final void maybeLogZenChange(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
boolean fromSystemOrSystemUi) {
mChangeState.init(prevInfo, newInfo, callingUid, fromSystemOrSystemUi);
if (mChangeState.shouldLogChanges()) {
maybeReassignCallingUid();
logChanges();
}
// clear out the state for a fresh start next time
mChangeState = new ZenModeEventLogger.ZenStateChanges();
}
/**
* Reassign callingUid in mChangeState if we have more specific information that warrants it
* (for instance, if the change is automatic and due to an automatic rule change).
*/
private void maybeReassignCallingUid() {
int userId = Process.INVALID_UID;
String packageName = null;
// For a manual rule, we consider reassigning the UID only when the call seems to come from
// the system and there is a non-null enabler in the new config.
// We don't consider the manual rule in the old config because if a manual rule is turning
// off with a call from system, that could easily be a user action to explicitly turn it off
if (mChangeState.getChangedRuleType() == RULE_TYPE_MANUAL) {
if (!mChangeState.mFromSystemOrSystemUi
|| mChangeState.getNewManualRuleEnabler() == null) {
return;
}
packageName = mChangeState.getNewManualRuleEnabler();
userId = mChangeState.mNewConfig.user; // mNewConfig must not be null if enabler exists
}
// The conditions where we should consider reassigning UID for an automatic rule change:
// - we've determined it's not a user action
// - our current best guess is that the calling uid is system/sysui
if (mChangeState.getChangedRuleType() == RULE_TYPE_AUTOMATIC) {
if (mChangeState.getIsUserAction() || !mChangeState.mFromSystemOrSystemUi) {
return;
}
// Only try to get the package UID if there's exactly one changed automatic rule. If
// there's more than one that changes simultaneously, this is likely to be a boot and
// we can leave it attributed to system.
ArrayMap<String, ZenModeDiff.RuleDiff> changedRules =
mChangeState.getChangedAutomaticRules();
if (changedRules.size() != 1) {
return;
}
Pair<String, Integer> ruleInfo = mChangeState.getRulePackageAndUser(
changedRules.keyAt(0),
changedRules.valueAt(0));
if (ruleInfo == null || ruleInfo.first.equals(ZenModeConfig.SYSTEM_AUTHORITY)) {
// leave system rules as-is
return;
}
packageName = ruleInfo.first;
userId = ruleInfo.second;
}
if (userId == Process.INVALID_UID || packageName == null) {
// haven't found anything to look up.
return;
}
try {
int uid = mPm.getPackageUidAsUser(packageName, userId);
mChangeState.mCallingUid = uid;
} catch (PackageManager.NameNotFoundException e) {
Slog.e(TAG, "unable to find package name " + packageName + " " + userId);
}
}
/**
* Actually log all changes stored in the current change state to statsd output. This method
* should not be used directly by callers; visible for override by subclasses.
*/
void logChanges() {
FrameworkStatsLog.write(FrameworkStatsLog.DND_STATE_CHANGED,
/* int32 event_id = 1 */ mChangeState.getEventId().getId(),
/* android.stats.dnd.ZenMode new_mode = 2 */ mChangeState.mNewZenMode,
/* android.stats.dnd.ZenMode previous_mode = 3 */ mChangeState.mPrevZenMode,
/* android.stats.dnd.RuleType rule_type = 4 */ mChangeState.getChangedRuleType(),
/* int32 num_rules_active = 5 */ mChangeState.getNumRulesActive(),
/* bool user_action = 6 */ mChangeState.getIsUserAction(),
/* int32 package_uid = 7 */ mChangeState.getPackageUid(),
/* DNDPolicyProto current_policy = 8 */ mChangeState.getDNDPolicyProto(),
/* bool are_channels_bypassing = 9 */ mChangeState.getAreChannelsBypassing());
}
/**
* Helper class for storing the set of information about a zen mode configuration at a specific
* time: the current zen mode setting, ZenModeConfig, and consolidated policy (a result of
* evaluating all active zen rules at the time).
*/
public static class ZenModeInfo {
final int mZenMode;
final ZenModeConfig mConfig;
final NotificationManager.Policy mPolicy;
ZenModeInfo(int zenMode, ZenModeConfig config, NotificationManager.Policy policy) {
mZenMode = zenMode;
// Store a copy of configs & policies to not accidentally pick up any further changes
mConfig = config != null ? config.copy() : null;
mPolicy = policy != null ? policy.copy() : null;
}
}
/**
* Class used to track overall changes in zen mode, since changes such as config updates happen
* in multiple stages (first changing the config, then re-evaluating zen mode and the
* consolidated policy), and which contains the logic of 1) whether to log the zen mode change
* and 2) deriving the properties to log.
*/
static class ZenStateChanges {
int mPrevZenMode = ZEN_MODE_UNKNOWN;
int mNewZenMode = ZEN_MODE_UNKNOWN;
ZenModeConfig mPrevConfig, mNewConfig;
NotificationManager.Policy mPrevPolicy, mNewPolicy;
int mCallingUid = Process.INVALID_UID;
boolean mFromSystemOrSystemUi = false;
private void init(ZenModeInfo prevInfo, ZenModeInfo newInfo, int callingUid,
boolean fromSystemOrSystemUi) {
// previous & new may be the same -- that would indicate that zen mode hasn't changed.
mPrevZenMode = prevInfo.mZenMode;
mNewZenMode = newInfo.mZenMode;
mPrevConfig = prevInfo.mConfig;
mNewConfig = newInfo.mConfig;
mPrevPolicy = prevInfo.mPolicy;
mNewPolicy = newInfo.mPolicy;
mCallingUid = callingUid;
mFromSystemOrSystemUi = fromSystemOrSystemUi;
}
/**
* Returns whether there is a policy diff represented by this change. This doesn't count
* if the previous policy is null, as that would indicate having no information rather than
* having no previous policy.
*/
private boolean hasPolicyDiff() {
return mPrevPolicy != null && !Objects.equals(mPrevPolicy, mNewPolicy);
}
/**
* Whether the set of changes encapsulated in this state should be logged. This should only
* be called after methods to store config and zen mode info.
*/
private boolean shouldLogChanges() {
// Did zen mode change from off to on or vice versa? If so, log in all cases.
if (zenModeFlipped()) {
return true;
}
// If zen mode didn't change, did the policy or number of active rules change? We only
// care about changes that take effect while zen mode is on, so make sure the current
// zen mode is not "OFF"
if (mNewZenMode == ZEN_MODE_OFF) {
return false;
}
return hasPolicyDiff() || hasRuleCountDiff();
}
// Does the difference in zen mode go from off to on or vice versa?
private boolean zenModeFlipped() {
if (mPrevZenMode == mNewZenMode) {
return false;
}
// then it flipped if one or the other is off. (there's only one off state; there are
// multiple states one could consider "on")
return mPrevZenMode == ZEN_MODE_OFF || mNewZenMode == ZEN_MODE_OFF;
}
// Helper methods below to fill out the atom contents below:
/**
* Based on the changes, returns the event ID corresponding to the change. Assumes that
* shouldLogChanges() is true and already checked (and will Log.wtf if not true).
*/
ZenStateChangedEvent getEventId() {
if (!shouldLogChanges()) {
Log.wtf(TAG, "attempt to get DNDStateChanged fields without shouldLog=true");
}
if (zenModeFlipped()) {
if (mPrevZenMode == ZEN_MODE_OFF) {
return ZenStateChangedEvent.DND_TURNED_ON;
} else {
return ZenStateChangedEvent.DND_TURNED_OFF;
}
}
// zen mode didn't change; we must be here because of a policy change or rule change
if (hasPolicyDiff() || hasChannelsBypassingDiff()) {
return ZenStateChangedEvent.DND_POLICY_CHANGED;
}
// Also no policy change, so it has to be a rule change
return ZenStateChangedEvent.DND_ACTIVE_RULES_CHANGED;
}
/**
* Based on the config diff, determine which type of rule changed (or "unknown" to indicate
* unknown or neither).
* In the (probably somewhat unusual) case that there are both, manual takes precedence over
* automatic.
*/
int getChangedRuleType() {
ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(mPrevConfig, mNewConfig);
if (!diff.hasDiff()) {
// no diff in the config. this probably shouldn't happen, but we can consider it
// unknown (given that if zen mode changes it is usually accompanied by some rule
// turning on or off, which should cause a config diff).
return RULE_TYPE_UNKNOWN;
}
ZenModeDiff.RuleDiff manualDiff = diff.getManualRuleDiff();
if (manualDiff != null && manualDiff.hasDiff()) {
// a diff in the manual rule doesn't *necessarily* mean that it's responsible for
// the change -- only if it's been added or removed.
if (manualDiff.wasAdded() || manualDiff.wasRemoved()) {
return RULE_TYPE_MANUAL;
}
}
ArrayMap<String, ZenModeDiff.RuleDiff> autoDiffs = diff.getAllAutomaticRuleDiffs();
if (autoDiffs != null) {
for (ZenModeDiff.RuleDiff d : autoDiffs.values()) {
if (d != null && d.hasDiff()) {
// If the rule became active or inactive, then this is probably relevant.
if (d.becameActive() || d.becameInactive()) {
return RULE_TYPE_AUTOMATIC;
}
}
}
}
return RULE_TYPE_UNKNOWN;
}
/**
* Returns whether the previous config and new config have a different number of active
* automatic or manual rules.
*/
private boolean hasRuleCountDiff() {
return numActiveRulesInConfig(mPrevConfig) != numActiveRulesInConfig(mNewConfig);
}
/**
* Get the number of active rules represented in a zen mode config. Because this is based
* on a config, this does not take into account the zen mode at the time of the config,
* which means callers need to take the zen mode into account for whether the rules are
* actually active.
*/
int numActiveRulesInConfig(ZenModeConfig config) {
// If the config is null, return early
if (config == null) {
return 0;
}
int rules = 0;
// Loop through the config and check:
// - does a manual rule exist? (if it's non-null, it's active)
// - how many automatic rules are active, as defined by isAutomaticActive()?
if (config.manualRule != null) {
rules++;
}
if (config.automaticRules != null) {
for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
if (rule != null && rule.isAutomaticActive()) {
rules++;
}
}
}
return rules;
}
// Determine the number of (automatic & manual) rules active after the change takes place.
int getNumRulesActive() {
// If the zen mode has turned off, that means nothing can be active.
if (mNewZenMode == ZEN_MODE_OFF) {
return 0;
}
return numActiveRulesInConfig(mNewConfig);
}
/**
* Return our best guess as to whether the changes observed are due to a user action.
* Note that this won't be 100% accurate as we can't necessarily distinguish between a
* system uid call indicating "user interacted with Settings" vs "a system app changed
* something automatically".
*/
boolean getIsUserAction() {
// Approach:
// - if manual rule turned on or off, the calling UID is system, and the new manual
// rule does not have an enabler set, guess that this is likely to be a user action.
// This may represent a system app turning on DND automatically, but we guess "user"
// in this case.
// - note that this has a known failure mode of "manual rule turning off
// automatically after the default time runs out". We currently have no way
// of distinguishing this case from a user manually turning off the rule.
// - the reason for checking the enabler field is that a call may look like it's
// coming from a system UID, but if an enabler is set then the request came
// from an external source. "enabler" will be blank when manual rule is turned
// on from Quick Settings or Settings.
// - if an automatic rule's state changes in whether it is "enabled", then
// that is probably a user action.
// - if an automatic rule goes from "not snoozing" to "snoozing", that is probably
// a user action; that means that the user temporarily turned off DND associated
// with that rule.
// - if an automatic rule becomes active but does *not* change in its enabled state
// (covered by a previous case anyway), we guess that this is an automatic change.
// - if a rule is added or removed and the call comes from the system, we guess that
// this is a user action (as system rules can't be added or removed without a user
// action).
switch (getChangedRuleType()) {
case RULE_TYPE_MANUAL:
// TODO(b/278888961): Distinguish the automatically-turned-off state
return mFromSystemOrSystemUi && (getNewManualRuleEnabler() == null);
case RULE_TYPE_AUTOMATIC:
for (ZenModeDiff.RuleDiff d : getChangedAutomaticRules().values()) {
if (d.wasAdded() || d.wasRemoved()) {
// If the change comes from system, a rule being added/removed indicates
// a likely user action. From an app, it's harder to know for sure.
return mFromSystemOrSystemUi;
}
ZenModeDiff.FieldDiff enabled = d.getDiffForField(
ZenModeDiff.RuleDiff.FIELD_ENABLED);
if (enabled != null && enabled.hasDiff()) {
return true;
}
ZenModeDiff.FieldDiff snoozing = d.getDiffForField(
ZenModeDiff.RuleDiff.FIELD_SNOOZING);
if (snoozing != null && snoozing.hasDiff() && (boolean) snoozing.to()) {
return true;
}
}
// If the change was in an automatic rule and none of the "probably triggered
// by a user" cases apply, then it's probably an automatic change.
return false;
case RULE_TYPE_UNKNOWN:
default:
}
// If the change wasn't in a rule, but was in the zen policy: consider to be user action
// if the calling uid is system
if (hasPolicyDiff() || hasChannelsBypassingDiff()) {
return mCallingUid == Process.SYSTEM_UID;
}
// don't know, or none of the other things triggered; assume not a user action
return false;
}
/**
* Get the package UID associated with this change, which is just the calling UID for the
* relevant method changes. This may get reset by ZenModeEventLogger, which has access to
* a PackageManager to get an appropriate UID for a package.
*/
int getPackageUid() {
return mCallingUid;
}
/**
* Convert the new policy to a DNDPolicyProto format for output in logs.
*/
byte[] getDNDPolicyProto() {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ProtoOutputStream proto = new ProtoOutputStream(bytes);
// While we don't expect this to be null at any point, guard against any weird cases.
if (mNewPolicy != null) {
proto.write(DNDPolicyProto.CALLS, toState(mNewPolicy.allowCalls()));
proto.write(DNDPolicyProto.REPEAT_CALLERS,
toState(mNewPolicy.allowRepeatCallers()));
proto.write(DNDPolicyProto.MESSAGES, toState(mNewPolicy.allowMessages()));
proto.write(DNDPolicyProto.CONVERSATIONS, toState(mNewPolicy.allowConversations()));
proto.write(DNDPolicyProto.REMINDERS, toState(mNewPolicy.allowReminders()));
proto.write(DNDPolicyProto.EVENTS, toState(mNewPolicy.allowEvents()));
proto.write(DNDPolicyProto.ALARMS, toState(mNewPolicy.allowAlarms()));
proto.write(DNDPolicyProto.MEDIA, toState(mNewPolicy.allowMedia()));
proto.write(DNDPolicyProto.SYSTEM, toState(mNewPolicy.allowSystem()));
proto.write(DNDPolicyProto.FULLSCREEN, toState(mNewPolicy.showFullScreenIntents()));
proto.write(DNDPolicyProto.LIGHTS, toState(mNewPolicy.showLights()));
proto.write(DNDPolicyProto.PEEK, toState(mNewPolicy.showPeeking()));
proto.write(DNDPolicyProto.STATUS_BAR, toState(mNewPolicy.showStatusBarIcons()));
proto.write(DNDPolicyProto.BADGE, toState(mNewPolicy.showBadges()));
proto.write(DNDPolicyProto.AMBIENT, toState(mNewPolicy.showAmbient()));
proto.write(DNDPolicyProto.NOTIFICATION_LIST,
toState(mNewPolicy.showInNotificationList()));
// Note: The DND policy proto uses the people type enum from *ZenPolicy* and not
// *NotificationManager.Policy* (which is the type of the consolidated policy).
// This applies to both call and message senders, but not conversation senders,
// where they use the same enum values.
proto.write(DNDPolicyProto.ALLOW_CALLS_FROM,
ZenModeConfig.getZenPolicySenders(mNewPolicy.allowCallsFrom()));
proto.write(DNDPolicyProto.ALLOW_MESSAGES_FROM,
ZenModeConfig.getZenPolicySenders(mNewPolicy.allowMessagesFrom()));
proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM,
mNewPolicy.allowConversationsFrom());
if (Flags.modesApi()) {
proto.write(DNDPolicyProto.ALLOW_CHANNELS,
mNewPolicy.allowPriorityChannels()
? ZenPolicy.CHANNEL_TYPE_PRIORITY
: ZenPolicy.CHANNEL_TYPE_NONE);
}
} else {
Log.wtf(TAG, "attempted to write zen mode log event with null policy");
}
proto.flush();
return bytes.toByteArray();
}
/**
* Get whether any channels are bypassing DND based on the current new policy.
*/
boolean getAreChannelsBypassing() {
if (mNewPolicy != null) {
return (mNewPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0;
}
return false;
}
private boolean hasChannelsBypassingDiff() {
boolean prevChannelsBypassing = mPrevPolicy != null
? (mPrevPolicy.state & STATE_CHANNELS_BYPASSING_DND) != 0 : false;
return prevChannelsBypassing != getAreChannelsBypassing();
}
/**
* helper method to turn a boolean allow or disallow state into STATE_ALLOW or
* STATE_DISALLOW (there is no concept of "unset" in NM.Policy.)
*/
private int toState(boolean allow) {
return allow ? ZenPolicy.STATE_ALLOW : ZenPolicy.STATE_DISALLOW;
}
/**
* Get the list of automatic rules that have any diff (as a List of ZenModeDiff.RuleDiff).
* Returns an empty list if there isn't anything.
*/
private @NonNull ArrayMap<String, ZenModeDiff.RuleDiff> getChangedAutomaticRules() {
ArrayMap<String, ZenModeDiff.RuleDiff> ruleDiffs = new ArrayMap<>();
ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(mPrevConfig, mNewConfig);
if (!diff.hasDiff()) {
return ruleDiffs;
}
ArrayMap<String, ZenModeDiff.RuleDiff> autoDiffs = diff.getAllAutomaticRuleDiffs();
if (autoDiffs != null) {
return autoDiffs;
}
return ruleDiffs;
}
/**
* Get the package name associated with this rule's owner, given its id and associated
* RuleDiff, as well as the user ID associated with the config it was found in. Returns null
* if none could be found.
*/
private Pair<String, Integer> getRulePackageAndUser(String id, ZenModeDiff.RuleDiff diff) {
// look for the rule info in the new config unless the rule was deleted.
ZenModeConfig configForSearch = mNewConfig;
if (diff.wasRemoved()) {
configForSearch = mPrevConfig;
}
if (configForSearch == null) {
return null;
}
ZenModeConfig.ZenRule rule = configForSearch.automaticRules.getOrDefault(id, null);
if (rule != null) {
if (rule.component != null) {
return new Pair(rule.component.getPackageName(), configForSearch.user);
}
if (rule.configurationActivity != null) {
return new Pair(rule.configurationActivity.getPackageName(),
configForSearch.user);
}
}
return null;
}
/**
* Get the package name listed as the manual rule "enabler", if it exists in the new config.
*/
private String getNewManualRuleEnabler() {
if (mNewConfig == null || mNewConfig.manualRule == null) {
return null;
}
return mNewConfig.manualRule.enabler;
}
/**
* Makes a copy for storing intermediate state for testing purposes.
*/
protected ZenStateChanges copy() {
ZenStateChanges copy = new ZenStateChanges();
copy.mPrevZenMode = mPrevZenMode;
copy.mNewZenMode = mNewZenMode;
copy.mPrevConfig = mPrevConfig.copy();
copy.mNewConfig = mNewConfig.copy();
copy.mPrevPolicy = mPrevPolicy.copy();
copy.mNewPolicy = mNewPolicy.copy();
copy.mCallingUid = mCallingUid;
copy.mFromSystemOrSystemUi = mFromSystemOrSystemUi;
return copy;
}
}
}