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