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