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 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 101 public void startDeactivation(OmtpVvmCarrierConfigHelper config) { 102 // VVM3 does not support deactivation. 103 // do nothing. 104 } 105 106 @Override 107 public boolean supportsProvisioning() { 108 return true; 109 } 110 111 @Override 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 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 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 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 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 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 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 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 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 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