1 /* 2 * Copyright (C) 2016 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.phone.vvm.omtp.protocol; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.net.Network; 22 import android.os.Bundle; 23 import android.telecom.PhoneAccountHandle; 24 import android.telephony.SmsManager; 25 import android.text.TextUtils; 26 import com.android.phone.VoicemailStatus; 27 import com.android.phone.common.mail.MessagingException; 28 import com.android.phone.settings.VisualVoicemailSettingsUtil; 29 import com.android.phone.settings.VoicemailChangePinActivity; 30 import com.android.phone.vvm.omtp.ActivationTask; 31 import com.android.phone.vvm.omtp.OmtpConstants; 32 import com.android.phone.vvm.omtp.OmtpEvents; 33 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper; 34 import com.android.phone.vvm.omtp.VisualVoicemailPreferences; 35 import com.android.phone.vvm.omtp.VvmLog; 36 import com.android.phone.vvm.omtp.imap.ImapHelper; 37 import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException; 38 import com.android.phone.vvm.omtp.sms.OmtpMessageSender; 39 import com.android.phone.vvm.omtp.sms.StatusMessage; 40 import com.android.phone.vvm.omtp.sms.Vvm3MessageSender; 41 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest; 42 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.NetworkWrapper; 43 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.RequestFailedException; 44 import java.io.IOException; 45 import java.security.SecureRandom; 46 import java.util.Locale; 47 48 /** 49 * A flavor of OMTP protocol with a different provisioning process 50 * 51 * Used by carriers such as Verizon Wireless 52 */ 53 public class Vvm3Protocol extends VisualVoicemailProtocol { 54 55 private static final String TAG = "Vvm3Protocol"; 56 57 private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED"; 58 private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd"; 59 private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS"; 60 private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url"; 61 62 private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; 63 private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s"; 64 private static final String IMAP_CLOSE_NUT = "CLOSE_NUT"; 65 66 private static final String ISO639_Spanish = "es"; 67 68 /** 69 * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link 70 * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, 71 * the user can self-provision visual voicemail service. For other response codes, the user must 72 * contact customer support to resolve the issue. 73 */ 74 private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2"; 75 76 // Default prompt level when using the telephone user interface. 77 // Standard prompt when the user call into the voicemail, and no prompts when someone else is 78 // leaving a voicemail. 79 private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5"; 80 private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6"; 81 82 private static final int DEFAULT_PIN_LENGTH = 6; 83 84 @Override startActivation(OmtpVvmCarrierConfigHelper config)85 public void startActivation(OmtpVvmCarrierConfigHelper config) { 86 // VVM3 does not support activation SMS. 87 // Send a status request which will start the provisioning process if the user is not 88 // provisioned. 89 VvmLog.i(TAG, "Activating"); 90 config.requestStatus(); 91 } 92 93 @Override startDeactivation(OmtpVvmCarrierConfigHelper config)94 public void startDeactivation(OmtpVvmCarrierConfigHelper config) { 95 // VVM3 does not support deactivation. 96 // do nothing. 97 } 98 99 @Override supportsProvisioning()100 public boolean supportsProvisioning() { 101 return true; 102 } 103 104 @Override startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, Bundle data)105 public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle, 106 OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, 107 Bundle data) { 108 VvmLog.i(TAG, "start vvm3 provisioning"); 109 if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) { 110 VvmLog.i(TAG, "Provisioning status: Unknown"); 111 if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE 112 .equals(message.getReturnCode())) { 113 VvmLog.i(TAG, "Self provisioning available, subscribing"); 114 new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe(); 115 } else { 116 config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN); 117 } 118 } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) { 119 VvmLog.i(TAG, "setting up new user"); 120 // Save the IMAP credentials in preferences so they are persistent and can be retrieved. 121 VisualVoicemailPreferences prefs = 122 new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle); 123 message.putStatus(prefs.edit()).apply(); 124 125 startProvisionNewUser(task, phoneAccountHandle, config, status, message); 126 } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) { 127 VvmLog.i(TAG, "User provisioned but not activated, disabling VVM"); 128 VisualVoicemailSettingsUtil 129 .setEnabled(config.getContext(), phoneAccountHandle, false); 130 } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) { 131 VvmLog.i(TAG, "User blocked"); 132 config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED); 133 } 134 } 135 136 @Override createMessageSender(SmsManager smsManager, short applicationPort, String destinationNumber)137 public OmtpMessageSender createMessageSender(SmsManager smsManager, short applicationPort, 138 String destinationNumber) { 139 return new Vvm3MessageSender(smsManager, applicationPort, destinationNumber); 140 } 141 142 @Override handleEvent(Context context, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event)143 public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, 144 VoicemailStatus.Editor status, OmtpEvents event) { 145 Vvm3EventHandler.handleEvent(context, config, status, event); 146 } 147 148 @Override getCommand(String command)149 public String getCommand(String command) { 150 if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) { 151 return IMAP_CHANGE_TUI_PWD_FORMAT; 152 } 153 if (command == OmtpConstants.IMAP_CLOSE_NUT) { 154 return IMAP_CLOSE_NUT; 155 } 156 if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) { 157 return IMAP_CHANGE_VM_LANG_FORMAT; 158 } 159 return super.getCommand(command); 160 } 161 162 @Override translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, Bundle data)163 public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, 164 Bundle data) { 165 // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned 166 // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status 167 // so provisioning can be done. 168 if (!SMS_EVENT_UNRECOGNIZED.equals(event)) { 169 return null; 170 } 171 if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) { 172 return null; 173 } 174 Bundle bundle = new Bundle(); 175 bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN); 176 bundle.putString(OmtpConstants.RETURN_CODE, 177 VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE); 178 String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY); 179 if (TextUtils.isEmpty(vmgUrl)) { 180 VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config"); 181 return null; 182 } 183 bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl); 184 VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS"); 185 return bundle; 186 } 187 startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message)188 private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle, 189 OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, 190 StatusMessage message) { 191 try (NetworkWrapper wrapper = VvmNetworkRequest 192 .getNetwork(config, phoneAccountHandle, status)) { 193 Network network = wrapper.get(); 194 195 VvmLog.i(TAG, "new user: network available"); 196 try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle, 197 network, status)) { 198 // VVM3 has inconsistent error language code to OMTP. Just issue a raw command 199 // here. 200 // TODO(b/29082671): use LocaleList 201 if (Locale.getDefault().getLanguage() 202 .equals(new Locale(ISO639_Spanish).getLanguage())) { 203 // Spanish 204 helper.changeVoicemailTuiLanguage( 205 VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS); 206 } else { 207 // English 208 helper.changeVoicemailTuiLanguage( 209 VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS); 210 } 211 VvmLog.i(TAG, "new user: language set"); 212 213 if (setPin(config.getContext(), phoneAccountHandle, helper, message)) { 214 // Only close new user tutorial if the PIN has been changed. 215 helper.closeNewUserTutorial(); 216 VvmLog.i(TAG, "new user: NUT closed"); 217 218 config.requestStatus(); 219 } 220 } catch (InitializingException | MessagingException | IOException e) { 221 config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED); 222 task.fail(); 223 VvmLog.e(TAG, e.toString()); 224 } 225 } catch (RequestFailedException e) { 226 config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); 227 task.fail(); 228 } 229 230 } 231 232 setPin(Context context, PhoneAccountHandle phoneAccountHandle, ImapHelper helper, StatusMessage message)233 private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle, 234 ImapHelper helper, StatusMessage message) 235 throws IOException, MessagingException { 236 String defaultPin = getDefaultPin(message); 237 if (defaultPin == null) { 238 VvmLog.i(TAG, "cannot generate default PIN"); 239 return false; 240 } 241 242 if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) { 243 // The pin was already set 244 VvmLog.i(TAG, "PIN already set"); 245 return true; 246 } 247 String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle)); 248 if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) { 249 VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin); 250 helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED); 251 } 252 VvmLog.i(TAG, "new user: PIN set"); 253 return true; 254 } 255 256 @Nullable getDefaultPin(StatusMessage message)257 private static String getDefaultPin(StatusMessage message) { 258 // The IMAP username is [phone number]@example.com 259 String username = message.getImapUserName(); 260 try { 261 String number = username.substring(0, username.indexOf('@')); 262 if (number.length() < 4) { 263 VvmLog.e(TAG, "unable to extract number from IMAP username"); 264 return null; 265 } 266 return "1" + number.substring(number.length() - 4); 267 } catch (StringIndexOutOfBoundsException e) { 268 VvmLog.e(TAG, "unable to extract number from IMAP username"); 269 return null; 270 } 271 272 } 273 getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle)274 private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) { 275 VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, 276 phoneAccountHandle); 277 // The OMTP pin length format is {min}-{max} 278 String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); 279 if (lengths.length == 2) { 280 try { 281 return Integer.parseInt(lengths[0]); 282 } catch (NumberFormatException e) { 283 return DEFAULT_PIN_LENGTH; 284 } 285 } 286 return DEFAULT_PIN_LENGTH; 287 } 288 generatePin(int length)289 private static String generatePin(int length) { 290 SecureRandom random = new SecureRandom(); 291 return String.format(Locale.US, "%010d", Math.abs(random.nextLong())) 292 .substring(0, length); 293 294 } 295 } 296