• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.dialer.notification;
18 
19 import android.annotation.TargetApi;
20 import android.app.Notification;
21 import android.app.NotificationChannel;
22 import android.app.NotificationChannelGroup;
23 import android.app.NotificationManager;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.media.AudioAttributes;
28 import android.net.Uri;
29 import android.os.Build.VERSION_CODES;
30 import android.provider.Settings;
31 import android.support.annotation.NonNull;
32 import android.support.annotation.Nullable;
33 import android.support.annotation.RequiresApi;
34 import android.support.annotation.StringDef;
35 import android.support.v4.os.BuildCompat;
36 import android.telecom.PhoneAccount;
37 import android.telecom.PhoneAccountHandle;
38 import android.telecom.TelecomManager;
39 import android.telephony.TelephonyManager;
40 import com.android.contacts.common.compat.TelephonyManagerCompat;
41 import com.android.dialer.buildtype.BuildType;
42 import com.android.dialer.common.LogUtil;
43 import com.android.dialer.common.concurrent.DialerExecutors;
44 import com.android.dialer.telecom.TelecomUtil;
45 import java.lang.annotation.Retention;
46 import java.lang.annotation.RetentionPolicy;
47 import java.util.List;
48 import java.util.Objects;
49 
50 /** Contains info on how to create {@link NotificationChannel NotificationChannels} */
51 public class NotificationChannelManager {
52 
53   private static final String PREFS_FILENAME = "NotificationChannelManager";
54   private static final String PREF_NEED_FIRST_INIT = "needFirstInit";
55   private static NotificationChannelManager instance;
56 
getInstance()57   public static NotificationChannelManager getInstance() {
58     if (instance == null) {
59       instance = new NotificationChannelManager();
60     }
61     return instance;
62   }
63 
64   /**
65    * Set the channel of notification appropriately. Will create the channel if it does not already
66    * exist. Safe to call pre-O (will no-op).
67    *
68    * <p>phoneAccount should only be null if channelName is {@link Channel#DEFAULT} or {@link
69    * Channel#MISSED_CALL} since these do not have account-specific settings.
70    */
71   @TargetApi(26)
applyChannel( @onNull Notification.Builder notification, @NonNull Context context, @Channel String channelName, @Nullable PhoneAccountHandle phoneAccount)72   public static void applyChannel(
73       @NonNull Notification.Builder notification,
74       @NonNull Context context,
75       @Channel String channelName,
76       @Nullable PhoneAccountHandle phoneAccount) {
77     checkNullity(channelName, phoneAccount);
78 
79     if (BuildCompat.isAtLeastO()) {
80       NotificationChannel channel =
81           NotificationChannelManager.getInstance().getChannel(context, channelName, phoneAccount);
82       notification.setChannelId(channel.getId());
83     }
84   }
85 
checkNullity( @hannel String channelName, @Nullable PhoneAccountHandle phoneAccount)86   private static void checkNullity(
87       @Channel String channelName, @Nullable PhoneAccountHandle phoneAccount) {
88     if (phoneAccount != null || channelAllowsNullPhoneAccountHandle(channelName)) {
89       return;
90     }
91 
92     // TODO (b/36568553): don't throw an exception once most cases have been identified
93     IllegalArgumentException exception =
94         new IllegalArgumentException(
95             "Phone account handle must not be null on channel " + channelName);
96     if (BuildType.get() == BuildType.RELEASE) {
97       LogUtil.e("NotificationChannelManager.applyChannel", null, exception);
98     } else {
99       throw exception;
100     }
101   }
102 
channelAllowsNullPhoneAccountHandle(@hannel String channelName)103   private static boolean channelAllowsNullPhoneAccountHandle(@Channel String channelName) {
104     switch (channelName) {
105       case Channel.DEFAULT:
106       case Channel.MISSED_CALL:
107         return true;
108       default:
109         return false;
110     }
111   }
112 
113   /** The base Channel IDs for {@link NotificationChannel} */
114   @Retention(RetentionPolicy.SOURCE)
115   @StringDef({
116     Channel.INCOMING_CALL,
117     Channel.ONGOING_CALL,
118     Channel.ONGOING_CALL_OLD,
119     Channel.MISSED_CALL,
120     Channel.VOICEMAIL,
121     Channel.EXTERNAL_CALL,
122     Channel.DEFAULT
123   })
124   public @interface Channel {
125     @Deprecated String ONGOING_CALL_OLD = "ongoingCall";
126     String INCOMING_CALL = "incomingCall";
127     String ONGOING_CALL = "ongoingCall2";
128     String MISSED_CALL = "missedCall";
129     String VOICEMAIL = "voicemail";
130     String EXTERNAL_CALL = "externalCall";
131     String DEFAULT = "default";
132   }
133 
134   @Channel
135   private static final String[] prepopulatedAccountChannels =
136       new String[] {Channel.INCOMING_CALL, Channel.ONGOING_CALL, Channel.VOICEMAIL};
137 
138   @Channel
139   private static final String[] prepopulatedGlobalChannels =
140       new String[] {Channel.MISSED_CALL, Channel.DEFAULT};
141 
NotificationChannelManager()142   private NotificationChannelManager() {}
143 
firstInitIfNeeded(@onNull Context context)144   public void firstInitIfNeeded(@NonNull Context context) {
145     if (BuildCompat.isAtLeastO()) {
146       DialerExecutors.createNonUiTaskBuilder(this::firstInitIfNeededSync)
147           .build()
148           .executeSerial(context);
149     }
150   }
151 
firstInitIfNeededSync(@onNull Context context)152   private boolean firstInitIfNeededSync(@NonNull Context context) {
153     if (needsFirstInit(context)) {
154       initChannels(context);
155       return true;
156     }
157     return false;
158   }
159 
needsFirstInit(@onNull Context context)160   public boolean needsFirstInit(@NonNull Context context) {
161     return (BuildCompat.isAtLeastO()
162         && getSharedPreferences(context).getBoolean(PREF_NEED_FIRST_INIT, true));
163   }
164 
165   @RequiresApi(VERSION_CODES.N)
getSharedPreferences(@onNull Context context)166   private SharedPreferences getSharedPreferences(@NonNull Context context) {
167     // Use device protected storage since in some cases this will need to be accessed while device
168     // is locked
169     context = context.createDeviceProtectedStorageContext();
170     return context.getSharedPreferences(PREFS_FILENAME, Context.MODE_PRIVATE);
171   }
172 
173   @RequiresApi(26)
getSettingsIntentForChannel( @onNull Context context, @Channel String channelName, PhoneAccountHandle accountHandle)174   public Intent getSettingsIntentForChannel(
175       @NonNull Context context, @Channel String channelName, PhoneAccountHandle accountHandle) {
176     checkNullity(channelName, accountHandle);
177     Intent intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
178     intent.putExtra(
179         Settings.EXTRA_CHANNEL_ID, getChannel(context, channelName, accountHandle).getId());
180     intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName());
181     return intent;
182   }
183 
184   @TargetApi(26)
185   @SuppressWarnings("AndroidApiChecker")
initChannels(@onNull Context context)186   public void initChannels(@NonNull Context context) {
187     if (!BuildCompat.isAtLeastO()) {
188       return;
189     }
190     LogUtil.enterBlock("NotificationChannelManager.initChannels");
191     List<PhoneAccountHandle> phoneAccounts = TelecomUtil.getCallCapablePhoneAccounts(context);
192 
193     // Remove notification channels for PhoneAccounts that don't exist anymore
194     NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
195     List<NotificationChannelGroup> notificationChannelGroups =
196         notificationManager.getNotificationChannelGroups();
197     notificationChannelGroups
198         .stream()
199         .filter(group -> !idExists(group.getId(), phoneAccounts))
200         .forEach(group -> deleteGroup(notificationManager, group));
201 
202     for (PhoneAccountHandle phoneAccountHandle : phoneAccounts) {
203       for (@Channel String channel : prepopulatedAccountChannels) {
204         getChannel(context, channel, phoneAccountHandle);
205       }
206     }
207 
208     for (@Channel String channel : prepopulatedGlobalChannels) {
209       getChannel(context, channel, null);
210     }
211     getSharedPreferences(context).edit().putBoolean(PREF_NEED_FIRST_INIT, false).apply();
212   }
213 
214   @TargetApi(26)
deleteGroup( @onNull NotificationManager notificationManager, @NonNull NotificationChannelGroup group)215   private void deleteGroup(
216       @NonNull NotificationManager notificationManager, @NonNull NotificationChannelGroup group) {
217     for (NotificationChannel channel : group.getChannels()) {
218       notificationManager.deleteNotificationChannel(channel.getId());
219     }
220     notificationManager.deleteNotificationChannelGroup(group.getId());
221   }
222 
idExists(String id, List<PhoneAccountHandle> phoneAccountHandles)223   private boolean idExists(String id, List<PhoneAccountHandle> phoneAccountHandles) {
224     for (PhoneAccountHandle handle : phoneAccountHandles) {
225       if (Objects.equals(handle.getId(), id)) {
226         return true;
227       }
228     }
229     return false;
230   }
231 
232   @NonNull
233   @RequiresApi(26)
getChannel( @onNull Context context, @Channel String channelName, @Nullable PhoneAccountHandle phoneAccount)234   private NotificationChannel getChannel(
235       @NonNull Context context,
236       @Channel String channelName,
237       @Nullable PhoneAccountHandle phoneAccount) {
238     String channelId = channelNameToId(channelName, phoneAccount);
239     NotificationChannel channel = getNotificationManager(context).getNotificationChannel(channelId);
240     if (channel == null) {
241       channel = createChannel(context, channelName, phoneAccount);
242     }
243     return channel;
244   }
245 
channelNameToId( @hannel String name, @Nullable PhoneAccountHandle phoneAccountHandle)246   private static String channelNameToId(
247       @Channel String name, @Nullable PhoneAccountHandle phoneAccountHandle) {
248     if (phoneAccountHandle == null) {
249       return name;
250     } else {
251       return name + ":" + phoneAccountHandle.getId();
252     }
253   }
254 
255   @RequiresApi(26)
createChannel( Context context, @Channel String channelName, @Nullable PhoneAccountHandle phoneAccountHandle)256   private NotificationChannel createChannel(
257       Context context,
258       @Channel String channelName,
259       @Nullable PhoneAccountHandle phoneAccountHandle) {
260     String channelId = channelNameToId(channelName, phoneAccountHandle);
261 
262     if (phoneAccountHandle != null) {
263       PhoneAccount account = getTelecomManager(context).getPhoneAccount(phoneAccountHandle);
264       NotificationChannelGroup group =
265           new NotificationChannelGroup(
266               phoneAccountHandle.getId(),
267               (account == null) ? phoneAccountHandle.getId() : account.getLabel().toString());
268       getNotificationManager(context)
269           .createNotificationChannelGroup(group); // No-op if already exists
270     } else if (!channelAllowsNullPhoneAccountHandle(channelName)) {
271       LogUtil.w(
272           "NotificationChannelManager.createChannel",
273           "Null PhoneAccountHandle with channel " + channelName);
274     }
275 
276     Uri silentRingtone = Uri.EMPTY;
277 
278     CharSequence name;
279     int importance;
280     boolean canShowBadge;
281     boolean lights;
282     boolean vibration;
283     Uri sound;
284     switch (channelName) {
285       case Channel.INCOMING_CALL:
286         name = context.getText(R.string.notification_channel_incoming_call);
287         importance = NotificationManager.IMPORTANCE_MAX;
288         canShowBadge = false;
289         lights = true;
290         vibration = false;
291         sound = silentRingtone;
292         break;
293       case Channel.MISSED_CALL:
294         name = context.getText(R.string.notification_channel_missed_call);
295         importance = NotificationManager.IMPORTANCE_DEFAULT;
296         canShowBadge = true;
297         lights = true;
298         vibration = true;
299         sound = silentRingtone;
300         break;
301       case Channel.ONGOING_CALL:
302         name = context.getText(R.string.notification_channel_ongoing_call);
303         importance = NotificationManager.IMPORTANCE_DEFAULT;
304         canShowBadge = false;
305         lights = false;
306         vibration = false;
307         sound = silentRingtone;
308         deleteOldOngoingCallChannelIfNeeded(context, phoneAccountHandle);
309         break;
310       case Channel.VOICEMAIL:
311         name = context.getText(R.string.notification_channel_voicemail);
312         importance = NotificationManager.IMPORTANCE_DEFAULT;
313         canShowBadge = true;
314         lights = true;
315         vibration =
316             TelephonyManagerCompat.isVoicemailVibrationEnabled(
317                 getTelephonyManager(context), phoneAccountHandle);
318         sound =
319             TelephonyManagerCompat.getVoicemailRingtoneUri(
320                 getTelephonyManager(context), phoneAccountHandle);
321         break;
322       case Channel.EXTERNAL_CALL:
323         name = context.getText(R.string.notification_channel_external_call);
324         importance = NotificationManager.IMPORTANCE_HIGH;
325         canShowBadge = false;
326         lights = true;
327         vibration = true;
328         sound = null;
329         break;
330       case Channel.DEFAULT:
331         name = context.getText(R.string.notification_channel_misc);
332         importance = NotificationManager.IMPORTANCE_DEFAULT;
333         canShowBadge = false;
334         lights = true;
335         vibration = true;
336         sound = null;
337         break;
338       default:
339         throw new IllegalArgumentException("Unknown channel: " + channelName);
340     }
341 
342     NotificationChannel channel = new NotificationChannel(channelId, name, importance);
343     channel.setShowBadge(canShowBadge);
344     if (sound != null) {
345       // silentRingtone acts as a sentinel value to indicate that setSound should still be called,
346       // but with a null value to indicate no sound.
347       channel.setSound(
348           sound.equals(silentRingtone) ? null : sound,
349           new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
350     }
351     channel.enableLights(lights);
352     channel.enableVibration(vibration);
353     getNotificationManager(context).createNotificationChannel(channel);
354     return channel;
355   }
356 
357   @RequiresApi(26)
deleteOldOngoingCallChannelIfNeeded( @onNull Context context, PhoneAccountHandle phoneAccountHandle)358   private void deleteOldOngoingCallChannelIfNeeded(
359       @NonNull Context context, PhoneAccountHandle phoneAccountHandle) {
360     String channelId = channelNameToId(Channel.ONGOING_CALL_OLD, phoneAccountHandle);
361     NotificationManager notificationManager = getNotificationManager(context);
362     NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
363     if (channel != null) {
364       LogUtil.i(
365           "NotificationManager.deleteOldOngoingCallChannelIfNeeded",
366           "Old ongoing channel found. Deleting to create new channel");
367       notificationManager.deleteNotificationChannel(channel.getId());
368     }
369   }
370 
getNotificationManager(@onNull Context context)371   private static NotificationManager getNotificationManager(@NonNull Context context) {
372     return context.getSystemService(NotificationManager.class);
373   }
374 
getTelephonyManager(@onNull Context context)375   private static TelephonyManager getTelephonyManager(@NonNull Context context) {
376     return context.getSystemService(TelephonyManager.class);
377   }
378 
getTelecomManager(@onNull Context context)379   private static TelecomManager getTelecomManager(@NonNull Context context) {
380     return context.getSystemService(TelecomManager.class);
381   }
382 }
383