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