1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.notification.modes;
18 
19 import android.annotation.Nullable;
20 import android.app.ActivityManager;
21 import android.app.AutomaticZenRule;
22 import android.app.INotificationManager;
23 import android.app.NotificationManager;
24 import android.content.Context;
25 import android.content.pm.ParceledListSlice;
26 import android.database.Cursor;
27 import android.net.Uri;
28 import android.os.ServiceManager;
29 import android.provider.ContactsContract;
30 import android.provider.Settings;
31 import android.service.notification.Condition;
32 import android.service.notification.ConversationChannelWrapper;
33 import android.service.notification.SystemZenRules;
34 import android.service.notification.ZenAdapters;
35 import android.service.notification.ZenModeConfig;
36 import android.util.Log;
37 
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.settings.R;
41 
42 import java.time.Duration;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.Map;
46 
47 /**
48  * Class used for Settings-NMS interactions related to Mode management.
49  *
50  * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
51  * into the unified {@link ZenMode} format.
52  */
53 class ZenModesBackend {
54 
55     private static final String TAG = "ZenModeBackend";
56 
57     @Nullable // Until first usage
58     private static ZenModesBackend sInstance;
59 
60     private final NotificationManager mNotificationManager;
61     static INotificationManager sINM = INotificationManager.Stub.asInterface(
62             ServiceManager.getService(Context.NOTIFICATION_SERVICE));
63 
64     private final Context mContext;
65 
getInstance(Context context)66     static ZenModesBackend getInstance(Context context) {
67         if (sInstance == null) {
68             sInstance = new ZenModesBackend(context.getApplicationContext());
69         }
70         return sInstance;
71     }
72 
ZenModesBackend(Context context)73     ZenModesBackend(Context context) {
74         mContext = context;
75         mNotificationManager = context.getSystemService(NotificationManager.class);
76     }
77 
getModes()78     List<ZenMode> getModes() {
79         ArrayList<ZenMode> modes = new ArrayList<>();
80         ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
81         modes.add(getManualDndMode(currentConfig));
82 
83         Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
84         for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
85             String ruleId = zenRuleEntry.getKey();
86             modes.add(new ZenMode(ruleId, zenRuleEntry.getValue(),
87                     isRuleActive(ruleId, currentConfig)));
88         }
89 
90         modes.sort((l, r) -> {
91             if (l.isManualDnd()) {
92                 return -1;
93             } else if (r.isManualDnd()) {
94                 return 1;
95             }
96             return l.getRule().getName().compareTo(r.getRule().getName());
97         });
98 
99         return modes;
100     }
101 
102     @Nullable
getMode(String id)103     ZenMode getMode(String id) {
104         ZenModeConfig currentConfig = mNotificationManager.getZenModeConfig();
105         if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
106             return getManualDndMode(currentConfig);
107         } else {
108             AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
109             if (rule == null) {
110                 return null;
111             }
112             return new ZenMode(id, rule, isRuleActive(id, currentConfig));
113         }
114     }
115 
getConversations(boolean onlyImportant)116     public ParceledListSlice<ConversationChannelWrapper> getConversations(boolean onlyImportant) {
117         try {
118             return sINM.getConversations(onlyImportant);
119         } catch (Exception e) {
120             Log.w(TAG, "Error calling NoMan", e);
121             return ParceledListSlice.emptyList();
122         }
123     }
124 
getStarredContacts()125     public List<String> getStarredContacts() {
126         Cursor cursor = null;
127         try {
128             cursor = queryStarredContactsData();
129             return getStarredContacts(cursor);
130         } finally {
131             if (cursor != null) {
132                 cursor.close();
133             }
134         }
135     }
136 
137     @VisibleForTesting
getStarredContacts(Cursor cursor)138     List<String> getStarredContacts(Cursor cursor) {
139         List<String> starredContacts = new ArrayList<>();
140         if (cursor != null && cursor.moveToFirst()) {
141             do {
142                 String contact = cursor.getString(0);
143                 starredContacts.add(contact != null ? contact :
144                         mContext.getString(R.string.zen_mode_starred_contacts_empty_name));
145 
146             } while (cursor.moveToNext());
147         }
148         return starredContacts;
149     }
150 
queryStarredContactsData()151     private Cursor queryStarredContactsData() {
152         return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
153                 new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY},
154                 ContactsContract.Data.STARRED + "=1", null,
155                 ContactsContract.Data.TIMES_CONTACTED);
156     }
157 
queryAllContactsData()158     Cursor queryAllContactsData() {
159         return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,
160                 new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY},
161                 null, null, null);
162     }
163 
getManualDndMode(ZenModeConfig config)164     private ZenMode getManualDndMode(ZenModeConfig config) {
165         ZenModeConfig.ZenRule manualRule = config.manualRule;
166         // TODO: b/333682392 - Replace with final strings for name & trigger description
167         AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
168                 mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
169                 .setType(manualRule.type)
170                 .setZenPolicy(manualRule.zenPolicy)
171                 .setDeviceEffects(manualRule.zenDeviceEffects)
172                 .setManualInvocationAllowed(manualRule.allowManualInvocation)
173                 .setConfigurationActivity(null) // No further settings
174                 .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
175                 .build();
176 
177         return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
178     }
179 
isRuleActive(String id, ZenModeConfig config)180     private static boolean isRuleActive(String id, ZenModeConfig config) {
181         if (config == null) {
182             // shouldn't happen if the config is coming from NM, but be safe
183             return false;
184         }
185         ZenModeConfig.ZenRule configRule = config.automaticRules.get(id);
186         return configRule != null && configRule.isAutomaticActive();
187     }
188 
updateMode(ZenMode mode)189     void updateMode(ZenMode mode) {
190         if (mode.isManualDnd()) {
191             try {
192                 NotificationManager.Policy dndPolicy =
193                         new ZenModeConfig().toNotificationPolicy(mode.getPolicy());
194                 mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
195 
196                 mNotificationManager.setManualZenRuleDeviceEffects(
197                         mode.getRule().getDeviceEffects());
198             } catch (Exception e) {
199                 Log.w(TAG, "Error updating manual mode", e);
200             }
201         } else {
202             mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
203                     /* fromUser= */ true);
204         }
205     }
206 
activateMode(ZenMode mode, @Nullable Duration forDuration)207     void activateMode(ZenMode mode, @Nullable Duration forDuration) {
208         if (mode.isManualDnd()) {
209             Uri durationConditionId = null;
210             if (forDuration != null) {
211                 durationConditionId = ZenModeConfig.toTimeCondition(mContext,
212                         (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
213             }
214             mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
215                     durationConditionId, TAG, /* fromUser= */ true);
216 
217         } else {
218             if (forDuration != null) {
219                 throw new IllegalArgumentException(
220                         "Only the manual DND mode can be activated for a specific duration");
221             }
222             mNotificationManager.setAutomaticZenRuleState(mode.getId(),
223                     new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
224                             Condition.SOURCE_USER_ACTION));
225         }
226     }
227 
deactivateMode(ZenMode mode)228     void deactivateMode(ZenMode mode) {
229         if (mode.isManualDnd()) {
230             // When calling with fromUser=true this will not snooze other modes.
231             mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
232                     /* fromUser= */ true);
233         } else {
234             // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
235             mNotificationManager.setAutomaticZenRuleState(mode.getId(),
236                     new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
237                             Condition.SOURCE_USER_ACTION));
238         }
239     }
240 
removeMode(ZenMode mode)241     void removeMode(ZenMode mode) {
242         if (!mode.canBeDeleted()) {
243             throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
244         }
245         mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
246     }
247 
248     /**
249      * Creates a new custom mode with the provided {@code name}. The mode will be "manual" (i.e.
250      * not have a schedule), this can be later updated by the user in the mode settings page.
251      *
252      * @return the created mode. Only {@code null} if creation failed due to an internal error
253      */
254     @Nullable
addCustomMode(String name)255     ZenMode addCustomMode(String name) {
256         ZenModeConfig.ScheduleInfo schedule = new ZenModeConfig.ScheduleInfo();
257         schedule.days = ZenModeConfig.ALL_DAYS;
258         schedule.startHour = 22;
259         schedule.endHour = 7;
260 
261         // TODO: b/326442408 - Create as "manual" (i.e. no trigger) instead of schedule-time.
262         AutomaticZenRule rule = new AutomaticZenRule.Builder(name,
263                 ZenModeConfig.toScheduleConditionId(schedule))
264                 .setPackage(ZenModeConfig.getScheduleConditionProvider().getPackageName())
265                 .setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR)
266                 .setOwner(ZenModeConfig.getScheduleConditionProvider())
267                 .setTriggerDescription(SystemZenRules.getTriggerDescriptionForScheduleTime(
268                         mContext, schedule))
269                 .setManualInvocationAllowed(true)
270                 .build();
271 
272         String ruleId = mNotificationManager.addAutomaticZenRule(rule);
273         return getMode(ruleId);
274     }
275 }
276