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 static java.nio.charset.StandardCharsets.UTF_8; 20 21 import android.Manifest.permission; 22 import android.annotation.TargetApi; 23 import android.app.NotificationChannel; 24 import android.app.NotificationManager; 25 import android.content.Context; 26 import android.media.AudioAttributes; 27 import android.os.Build.VERSION_CODES; 28 import android.provider.Settings; 29 import android.support.annotation.NonNull; 30 import android.support.annotation.Nullable; 31 import android.support.annotation.RequiresPermission; 32 import android.support.annotation.VisibleForTesting; 33 import android.support.v4.os.BuildCompat; 34 import android.telecom.PhoneAccount; 35 import android.telecom.PhoneAccountHandle; 36 import android.telecom.TelecomManager; 37 import android.telephony.TelephonyManager; 38 import android.text.TextUtils; 39 import android.util.ArraySet; 40 import com.android.dialer.common.Assert; 41 import com.android.dialer.common.LogUtil; 42 import com.android.dialer.util.PermissionsUtil; 43 import java.security.MessageDigest; 44 import java.security.NoSuchAlgorithmException; 45 import java.util.ArrayList; 46 import java.util.List; 47 import java.util.Set; 48 49 /** Utilities for working with voicemail channels. */ 50 @TargetApi(VERSION_CODES.O) 51 public final class VoicemailChannelUtils { 52 @VisibleForTesting static final String GLOBAL_VOICEMAIL_CHANNEL_ID = "phone_voicemail"; 53 private static final String PER_ACCOUNT_VOICEMAIL_CHANNEL_ID_PREFIX = "phone_voicemail_account_"; 54 private static final char[] hexDigits = "0123456789abcdef".toCharArray(); 55 56 /** 57 * Returns a String representation of the hashed value of the PhoneAccountHandle's id (the 58 * Sim ICC ID). 59 * In case it fails to hash the id it will return an empty string. 60 */ getHashedPhoneAccountId(@onNull PhoneAccountHandle handle)61 public static String getHashedPhoneAccountId(@NonNull PhoneAccountHandle handle) { 62 byte[] handleBytes = handle.getId().getBytes(UTF_8); 63 try { 64 byte[] hashedBytes = MessageDigest.getInstance("SHA-256").digest(handleBytes); 65 return byteArrayToHexString(hashedBytes); 66 } catch (NoSuchAlgorithmException e) { 67 LogUtil.e("VoicemailChannelUtils.getHashedPhoneAccountId", 68 "NoSuchAlgorithmException throw! Returning empty string!"); 69 return ""; 70 } 71 } 72 73 @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission getAllChannelIds(@onNull Context context)74 static Set<String> getAllChannelIds(@NonNull Context context) { 75 Assert.checkArgument(BuildCompat.isAtLeastO()); 76 Assert.isNotNull(context); 77 78 Set<String> result = new ArraySet<>(); 79 if (isSingleSimDevice(context)) { 80 result.add(GLOBAL_VOICEMAIL_CHANNEL_ID); 81 } else { 82 for (PhoneAccountHandle handle : getAllEligableAccounts(context)) { 83 result.add(getChannelIdForAccount(handle)); 84 } 85 } 86 return result; 87 } 88 89 @SuppressWarnings("MissingPermission") // isSingleSimDevice() returns true if no permission createAllChannels(@onNull Context context)90 static void createAllChannels(@NonNull Context context) { 91 Assert.checkArgument(BuildCompat.isAtLeastO()); 92 Assert.isNotNull(context); 93 94 if (isSingleSimDevice(context)) { 95 createGlobalVoicemailChannel(context); 96 } else { 97 for (PhoneAccountHandle handle : getAllEligableAccounts(context)) { 98 createVoicemailChannelForAccount(context, handle); 99 } 100 } 101 } 102 103 @NonNull getChannelId(@onNull Context context, @Nullable PhoneAccountHandle handle)104 static String getChannelId(@NonNull Context context, @Nullable PhoneAccountHandle handle) { 105 Assert.checkArgument(BuildCompat.isAtLeastO()); 106 Assert.isNotNull(context); 107 108 // Most devices we deal with have a single SIM slot. No need to distinguish between phone 109 // accounts. 110 if (isSingleSimDevice(context)) { 111 return GLOBAL_VOICEMAIL_CHANNEL_ID; 112 } 113 114 // We can get a null phone account at random points (modem reboot, etc...). Gracefully degrade 115 // by using the default channel. 116 if (handle == null) { 117 LogUtil.i( 118 "VoicemailChannelUtils.getChannelId", 119 "no phone account on a multi-SIM device, using default channel"); 120 return NotificationChannelId.DEFAULT; 121 } 122 123 // Voicemail notifications should always be associated with a SIM based phone account. 124 if (!isChannelAllowedForAccount(context, handle)) { 125 LogUtil.i( 126 "VoicemailChannelUtils.getChannelId", 127 "phone account is not for a SIM, using default channel"); 128 return NotificationChannelId.DEFAULT; 129 } 130 131 // Now we're in the multi-SIM case. 132 String channelId = getChannelIdForAccount(handle); 133 if (!doesChannelExist(context, channelId)) { 134 LogUtil.i( 135 "VoicemailChannelUtils.getChannelId", 136 "voicemail channel not found for phone account (possible SIM swap?), creating a new one"); 137 createVoicemailChannelForAccount(context, handle); 138 } 139 return channelId; 140 } 141 doesChannelExist(@onNull Context context, @NonNull String channelId)142 private static boolean doesChannelExist(@NonNull Context context, @NonNull String channelId) { 143 return context.getSystemService(NotificationManager.class).getNotificationChannel(channelId) 144 != null; 145 } 146 getChannelIdForAccount(@onNull PhoneAccountHandle handle)147 private static String getChannelIdForAccount(@NonNull PhoneAccountHandle handle) { 148 Assert.isNotNull(handle); 149 return PER_ACCOUNT_VOICEMAIL_CHANNEL_ID_PREFIX 150 + ":" 151 + getHashedPhoneAccountId(handle); 152 } 153 byteArrayToHexString(byte[] bytes)154 private static String byteArrayToHexString(byte[] bytes) { 155 StringBuilder sb = new StringBuilder(2 * bytes.length); 156 for (byte b : bytes) { 157 sb.append(hexDigits[(b >> 4) & 0xf]).append(hexDigits[b & 0xf]); 158 } 159 return sb.toString(); 160 } 161 162 /** 163 * Creates a voicemail channel but doesn't associate it with a SIM. For devices with only one SIM 164 * slot this is ideal because there won't be duplication in the settings UI. 165 */ createGlobalVoicemailChannel(@onNull Context context)166 private static void createGlobalVoicemailChannel(@NonNull Context context) { 167 NotificationChannel channel = newChannel(context, GLOBAL_VOICEMAIL_CHANNEL_ID, null); 168 migrateGlobalVoicemailSoundSettings(context, channel); 169 context.getSystemService(NotificationManager.class).createNotificationChannel(channel); 170 } 171 172 @SuppressWarnings("MissingPermission") // checked with PermissionsUtil migrateGlobalVoicemailSoundSettings( Context context, NotificationChannel channel)173 private static void migrateGlobalVoicemailSoundSettings( 174 Context context, NotificationChannel channel) { 175 if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) { 176 LogUtil.i( 177 "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", 178 "missing phone permission, not migrating sound settings"); 179 return; 180 } 181 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 182 PhoneAccountHandle handle = 183 telecomManager.getDefaultOutgoingPhoneAccount(PhoneAccount.SCHEME_TEL); 184 if (handle == null) { 185 LogUtil.i( 186 "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", 187 "phone account is null, not migrating sound settings"); 188 return; 189 } 190 if (!isChannelAllowedForAccount(context, handle)) { 191 LogUtil.i( 192 "VoicemailChannelUtils.migrateGlobalVoicemailSoundSettings", 193 "phone account is not eligable, not migrating sound settings"); 194 return; 195 } 196 migrateVoicemailSoundSettings(context, channel, handle); 197 } 198 199 @RequiresPermission(permission.READ_PHONE_STATE) getAllEligableAccounts(@onNull Context context)200 private static List<PhoneAccountHandle> getAllEligableAccounts(@NonNull Context context) { 201 List<PhoneAccountHandle> handles = new ArrayList<>(); 202 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 203 for (PhoneAccountHandle handle : telecomManager.getCallCapablePhoneAccounts()) { 204 if (isChannelAllowedForAccount(context, handle)) { 205 handles.add(handle); 206 } 207 } 208 return handles; 209 } 210 createVoicemailChannelForAccount( @onNull Context context, @NonNull PhoneAccountHandle handle)211 private static void createVoicemailChannelForAccount( 212 @NonNull Context context, @NonNull PhoneAccountHandle handle) { 213 PhoneAccount phoneAccount = 214 context.getSystemService(TelecomManager.class).getPhoneAccount(handle); 215 if (phoneAccount == null) { 216 return; 217 } 218 NotificationChannel channel = 219 newChannel(context, getChannelIdForAccount(handle), phoneAccount.getLabel()); 220 migrateVoicemailSoundSettings(context, channel, handle); 221 context.getSystemService(NotificationManager.class).createNotificationChannel(channel); 222 } 223 migrateVoicemailSoundSettings( @onNull Context context, @NonNull NotificationChannel channel, @NonNull PhoneAccountHandle handle)224 private static void migrateVoicemailSoundSettings( 225 @NonNull Context context, 226 @NonNull NotificationChannel channel, 227 @NonNull PhoneAccountHandle handle) { 228 TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); 229 channel.enableVibration(telephonyManager.isVoicemailVibrationEnabled(handle)); 230 channel.setSound( 231 telephonyManager.getVoicemailRingtoneUri(handle), 232 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()); 233 } 234 isChannelAllowedForAccount( @onNull Context context, @NonNull PhoneAccountHandle handle)235 private static boolean isChannelAllowedForAccount( 236 @NonNull Context context, @NonNull PhoneAccountHandle handle) { 237 PhoneAccount phoneAccount = 238 context.getSystemService(TelecomManager.class).getPhoneAccount(handle); 239 if (phoneAccount == null) { 240 return false; 241 } 242 if (!phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { 243 return false; 244 } 245 return true; 246 } 247 newChannel( @onNull Context context, @NonNull String channelId, @Nullable CharSequence nameSuffix)248 private static NotificationChannel newChannel( 249 @NonNull Context context, @NonNull String channelId, @Nullable CharSequence nameSuffix) { 250 CharSequence name = context.getText(R.string.notification_channel_voicemail); 251 // TODO(sail): Use a string resource template after v10. 252 if (!TextUtils.isEmpty(nameSuffix)) { 253 name = TextUtils.concat(name, ": ", nameSuffix); 254 } 255 256 NotificationChannel channel = 257 new NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT); 258 channel.setShowBadge(true); 259 channel.enableLights(true); 260 channel.enableVibration(true); 261 channel.setSound( 262 Settings.System.DEFAULT_NOTIFICATION_URI, 263 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()); 264 return channel; 265 } 266 isSingleSimDevice(@onNull Context context)267 private static boolean isSingleSimDevice(@NonNull Context context) { 268 if (!PermissionsUtil.hasReadPhoneStatePermissions(context)) { 269 return true; 270 } 271 return context.getSystemService(TelephonyManager.class).getPhoneCount() <= 1; 272 } 273 VoicemailChannelUtils()274 private VoicemailChannelUtils() {} 275 } 276