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 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 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 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 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 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 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 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 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 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 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 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