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