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