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.impl.ActivationTask; 30 import com.android.voicemail.impl.OmtpConstants; 31 import com.android.voicemail.impl.OmtpEvents; 32 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper; 33 import com.android.voicemail.impl.VisualVoicemailPreferences; 34 import com.android.voicemail.impl.VoicemailStatus; 35 import com.android.voicemail.impl.VvmLog; 36 import com.android.voicemail.impl.imap.ImapHelper; 37 import com.android.voicemail.impl.imap.ImapHelper.InitializingException; 38 import com.android.voicemail.impl.mail.MessagingException; 39 import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil; 40 import com.android.voicemail.impl.settings.VoicemailChangePinActivity; 41 import com.android.voicemail.impl.sms.OmtpMessageSender; 42 import com.android.voicemail.impl.sms.StatusMessage; 43 import com.android.voicemail.impl.sms.Vvm3MessageSender; 44 import com.android.voicemail.impl.sync.VvmNetworkRequest; 45 import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper; 46 import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException; 47 import com.android.voicemail.impl.utils.LoggerUtils; 48 import java.io.IOException; 49 import java.security.SecureRandom; 50 import 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) 58 public 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 startActivation( OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent)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 startDeactivation(OmtpVvmCarrierConfigHelper config)100 public void startDeactivation(OmtpVvmCarrierConfigHelper config) { 101 // VVM3 does not support deactivation. 102 // do nothing. 103 } 104 105 @Override supportsProvisioning()106 public boolean supportsProvisioning() { 107 return true; 108 } 109 110 @Override startProvisioning( ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, Bundle data)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 LoggerUtils.logImpressionOnMainThread( 120 config.getContext(), DialerImpression.Type.VVM_PROVISIONING_STARTED); 121 if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) { 122 VvmLog.i(TAG, "Provisioning status: Unknown"); 123 if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE.equals(message.getReturnCode())) { 124 VvmLog.i(TAG, "Self provisioning available, subscribing"); 125 new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe(); 126 } else { 127 config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN); 128 } 129 } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) { 130 VvmLog.i(TAG, "setting up new user"); 131 // Save the IMAP credentials in preferences so they are persistent and can be retrieved. 132 VisualVoicemailPreferences prefs = 133 new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle); 134 message.putStatus(prefs.edit()).apply(); 135 136 startProvisionNewUser(task, phoneAccountHandle, config, status, message); 137 } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) { 138 VvmLog.i(TAG, "User provisioned but not activated, disabling VVM"); 139 VisualVoicemailSettingsUtil.setEnabled(config.getContext(), phoneAccountHandle, false); 140 } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) { 141 VvmLog.i(TAG, "User blocked"); 142 config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED); 143 } 144 } 145 146 @Override createMessageSender( Context context, PhoneAccountHandle phoneAccountHandle, short applicationPort, String destinationNumber)147 public OmtpMessageSender createMessageSender( 148 Context context, 149 PhoneAccountHandle phoneAccountHandle, 150 short applicationPort, 151 String destinationNumber) { 152 return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, destinationNumber); 153 } 154 155 @Override handleEvent( Context context, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event)156 public void handleEvent( 157 Context context, 158 OmtpVvmCarrierConfigHelper config, 159 VoicemailStatus.Editor status, 160 OmtpEvents event) { 161 Vvm3EventHandler.handleEvent(context, config, status, event); 162 } 163 164 @Override getCommand(String command)165 public String getCommand(String command) { 166 switch (command) { 167 case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT: 168 return IMAP_CHANGE_TUI_PWD_FORMAT; 169 case OmtpConstants.IMAP_CLOSE_NUT: 170 return IMAP_CLOSE_NUT; 171 case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT: 172 return IMAP_CHANGE_VM_LANG_FORMAT; 173 default: 174 return super.getCommand(command); 175 } 176 } 177 178 @Override translateStatusSmsBundle( OmtpVvmCarrierConfigHelper config, String event, Bundle data)179 public Bundle translateStatusSmsBundle( 180 OmtpVvmCarrierConfigHelper config, String event, Bundle data) { 181 // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned 182 // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status 183 // so provisioning can be done. 184 if (!SMS_EVENT_UNRECOGNIZED.equals(event)) { 185 return null; 186 } 187 if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) { 188 return null; 189 } 190 Bundle bundle = new Bundle(); 191 bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN); 192 bundle.putString( 193 OmtpConstants.RETURN_CODE, VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE); 194 String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY); 195 if (TextUtils.isEmpty(vmgUrl)) { 196 VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config"); 197 return null; 198 } 199 bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl); 200 VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS"); 201 return bundle; 202 } 203 startProvisionNewUser( ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message)204 private void startProvisionNewUser( 205 ActivationTask task, 206 PhoneAccountHandle phoneAccountHandle, 207 OmtpVvmCarrierConfigHelper config, 208 VoicemailStatus.Editor status, 209 StatusMessage message) { 210 try (NetworkWrapper wrapper = 211 VvmNetworkRequest.getNetwork(config, phoneAccountHandle, status)) { 212 Network network = wrapper.get(); 213 214 VvmLog.i(TAG, "new user: network available"); 215 try (ImapHelper helper = 216 new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) { 217 // VVM3 has inconsistent error language code to OMTP. Just issue a raw command 218 // here. 219 // TODO(b/29082671): use LocaleList 220 if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) { 221 // Spanish 222 helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS); 223 } else { 224 // English 225 helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS); 226 } 227 VvmLog.i(TAG, "new user: language set"); 228 229 if (setPin(config.getContext(), phoneAccountHandle, helper, message)) { 230 // Only close new user tutorial if the PIN has been changed. 231 helper.closeNewUserTutorial(); 232 VvmLog.i(TAG, "new user: NUT closed"); 233 LoggerUtils.logImpressionOnMainThread( 234 config.getContext(), DialerImpression.Type.VVM_PROVISIONING_COMPLETED); 235 config.requestStatus(null); 236 } 237 } catch (InitializingException | MessagingException | IOException e) { 238 config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED); 239 task.fail(); 240 VvmLog.e(TAG, e.toString()); 241 } 242 } catch (RequestFailedException e) { 243 config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); 244 task.fail(); 245 } 246 } 247 setPin( Context context, PhoneAccountHandle phoneAccountHandle, ImapHelper helper, StatusMessage message)248 private static boolean setPin( 249 Context context, 250 PhoneAccountHandle phoneAccountHandle, 251 ImapHelper helper, 252 StatusMessage message) 253 throws IOException, MessagingException { 254 String defaultPin = getDefaultPin(message); 255 if (defaultPin == null) { 256 VvmLog.i(TAG, "cannot generate default PIN"); 257 return false; 258 } 259 260 if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) { 261 // The pin was already set 262 VvmLog.i(TAG, "PIN already set"); 263 return true; 264 } 265 String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle)); 266 if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) { 267 VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin); 268 helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED); 269 } 270 VvmLog.i(TAG, "new user: PIN set"); 271 return true; 272 } 273 274 @Nullable getDefaultPin(StatusMessage message)275 private static String getDefaultPin(StatusMessage message) { 276 // The IMAP username is [phone number]@example.com 277 String username = message.getImapUserName(); 278 try { 279 String number = username.substring(0, username.indexOf('@')); 280 if (number.length() < 4) { 281 VvmLog.e(TAG, "unable to extract number from IMAP username"); 282 return null; 283 } 284 return "1" + number.substring(number.length() - 4); 285 } catch (StringIndexOutOfBoundsException e) { 286 VvmLog.e(TAG, "unable to extract number from IMAP username"); 287 return null; 288 } 289 } 290 getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle)291 private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) { 292 VisualVoicemailPreferences preferences = 293 new VisualVoicemailPreferences(context, phoneAccountHandle); 294 // The OMTP pin length format is {min}-{max} 295 String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); 296 if (lengths.length == 2) { 297 try { 298 return Integer.parseInt(lengths[0]); 299 } catch (NumberFormatException e) { 300 return DEFAULT_PIN_LENGTH; 301 } 302 } 303 return DEFAULT_PIN_LENGTH; 304 } 305 generatePin(int length)306 private static String generatePin(int length) { 307 SecureRandom random = new SecureRandom(); 308 return String.format(Locale.US, "%010d", Math.abs(random.nextLong())).substring(0, length); 309 } 310 } 311