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