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