diff options
Diffstat (limited to 'java/com/android/voicemailomtp/protocol')
8 files changed, 1184 insertions, 0 deletions
diff --git a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java new file mode 100644 index 000000000..48ed99709 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.sms.OmtpCvvmMessageSender; +import com.android.voicemailomtp.sms.OmtpMessageSender; + +/** + * A flavor of OMTP protocol with a different mobile originated (MO) format + * + * Used by carriers such as T-Mobile + */ +public class CvvmProtocol extends VisualVoicemailProtocol { + + private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; + private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s"; + private static String IMAP_CLOSE_NUT = "CLOSE_NUT"; + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new OmtpCvvmMessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber); + } + + @Override + public String getCommand(String command) { + if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) { + return IMAP_CHANGE_TUI_PWD_FORMAT; + } + if (command == OmtpConstants.IMAP_CLOSE_NUT) { + return IMAP_CLOSE_NUT; + } + if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) { + return IMAP_CHANGE_VM_LANG_FORMAT; + } + return super.getCommand(command); + } +} diff --git a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java new file mode 100644 index 000000000..d88a23285 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.OmtpStandardMessageSender; + +public class OmtpProtocol extends VisualVoicemailProtocol { + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new OmtpStandardMessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber, + null, OmtpConstants.PROTOCOL_VERSION1_1, null); + } +} diff --git a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java new file mode 100644 index 000000000..4fca199bf --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.telephony.SmsManager; +import android.text.TextUtils; + +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.sms.OmtpMessageSender; + +public class ProtocolHelper { + + private static final String TAG = "ProtocolHelper"; + + public static OmtpMessageSender getMessageSender(VisualVoicemailProtocol protocol, + OmtpVvmCarrierConfigHelper config) { + + int applicationPort = config.getApplicationPort(); + String destinationNumber = config.getDestinationNumber(); + if (TextUtils.isEmpty(destinationNumber)) { + VvmLog.w(TAG, "No destination number for this carrier."); + return null; + } + + return protocol.createMessageSender(config.getContext(), config.getPhoneAccountHandle(), + (short) applicationPort, destinationNumber); + } +} diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java new file mode 100644 index 000000000..9ff2ed167 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.DefaultOmtpEventHandler; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.StatusMessage; + +public abstract class VisualVoicemailProtocol { + + /** + * Activation should cause the carrier to respond with a STATUS SMS. + */ + public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmActivation(sentIntent); + } + } + + public void startDeactivation(OmtpVvmCarrierConfigHelper config) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmDeactivation(null); + } + } + + public boolean supportsProvisioning() { + return false; + } + + public void startProvisioning(ActivationTask task, PhoneAccountHandle handle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor editor, StatusMessage message, + Bundle data) { + // Do nothing + } + + public void requestStatus(OmtpVvmCarrierConfigHelper config, + @Nullable PendingIntent sentIntent) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmStatus(sentIntent); + } + } + + public abstract OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, + short applicationPort, String destinationNumber); + + /** + * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI + * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD. + * + * @param command A String command in {@link com.android.voicemailomtp.OmtpConstants}, the exact + * instance should be used instead of its' value. + * @returns Translated command, or {@code null} if not available in this protocol + */ + public String getCommand(String command) { + return command; + } + + public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + DefaultOmtpEventHandler.handleEvent(context, config, status, event); + } + + /** + * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into + * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated. + */ + @Nullable + public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, + Bundle data) { + return null; + } +} diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java new file mode 100644 index 000000000..b74f503c6 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.content.res.Resources; +import android.support.annotation.Nullable; +import android.telephony.TelephonyManager; +import com.android.voicemailomtp.VvmLog; + +public class VisualVoicemailProtocolFactory { + + private static final String TAG = "VvmProtocolFactory"; + + private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3"; + + @Nullable + public static VisualVoicemailProtocol create(Resources resources, String type) { + if (type == null) { + return null; + } + switch (type) { + case TelephonyManager.VVM_TYPE_OMTP: + return new OmtpProtocol(); + case TelephonyManager.VVM_TYPE_CVVM: + return new CvvmProtocol(); + case VVM_TYPE_VVM3: + return new Vvm3Protocol(); + default: + VvmLog.e(TAG, "Unexpected visual voicemail type: " + type); + } + return null; + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java new file mode 100644 index 000000000..72646386c --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.util.Log; +import com.android.voicemailomtp.DefaultOmtpEventHandler; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpEvents.Type; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.settings.VoicemailChangePinActivity; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom + * error codes into the voicemail status table so support on the dialer side is required. + * + * TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured. + */ +public class Vvm3EventHandler { + + private static final String TAG = "Vvm3EventHandler"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({VMS_DNS_FAILURE, VMG_DNS_FAILURE, SPG_DNS_FAILURE, VMS_NO_CELLULAR, VMG_NO_CELLULAR, + SPG_NO_CELLULAR, VMS_TIMEOUT, VMG_TIMEOUT, STATUS_SMS_TIMEOUT, SUBSCRIBER_BLOCKED, + UNKNOWN_USER, UNKNOWN_DEVICE, INVALID_PASSWORD, MAILBOX_NOT_INITIALIZED, + SERVICE_NOT_PROVISIONED, SERVICE_NOT_ACTIVATED, USER_BLOCKED, IMAP_GETQUOTA_ERROR, + IMAP_SELECT_ERROR, IMAP_ERROR, VMG_INTERNAL_ERROR, VMG_DB_ERROR, + VMG_COMMUNICATION_ERROR, SPG_URL_NOT_FOUND, VMG_UNKNOWN_ERROR, PIN_NOT_SET}) + public @interface ErrorCode { + + } + + public static final int VMS_DNS_FAILURE = -9001; + public static final int VMG_DNS_FAILURE = -9002; + public static final int SPG_DNS_FAILURE = -9003; + public static final int VMS_NO_CELLULAR = -9004; + public static final int VMG_NO_CELLULAR = -9005; + public static final int SPG_NO_CELLULAR = -9006; + public static final int VMS_TIMEOUT = -9007; + public static final int VMG_TIMEOUT = -9008; + public static final int STATUS_SMS_TIMEOUT = -9009; + + public static final int SUBSCRIBER_BLOCKED = -9990; + public static final int UNKNOWN_USER = -9991; + public static final int UNKNOWN_DEVICE = -9992; + public static final int INVALID_PASSWORD = -9993; + public static final int MAILBOX_NOT_INITIALIZED = -9994; + public static final int SERVICE_NOT_PROVISIONED = -9995; + public static final int SERVICE_NOT_ACTIVATED = -9996; + public static final int USER_BLOCKED = -9998; + public static final int IMAP_GETQUOTA_ERROR = -9997; + public static final int IMAP_SELECT_ERROR = -9989; + public static final int IMAP_ERROR = -9999; + + public static final int VMG_INTERNAL_ERROR = -101; + public static final int VMG_DB_ERROR = -102; + public static final int VMG_COMMUNICATION_ERROR = -103; + public static final int SPG_URL_NOT_FOUND = -301; + + // Non VVM3 codes: + public static final int VMG_UNKNOWN_ERROR = -1; + public static final int PIN_NOT_SET = -100; + // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer + // support. + public static final int SUBSCRIBER_UNKNOWN = -99; + + + public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + boolean handled = false; + switch (event.getType()) { + case Type.CONFIGURATION: + handled = handleConfigurationEvent(context, status, event); + break; + case Type.DATA_CHANNEL: + handled = handleDataChannelEvent(status, event); + break; + case Type.NOTIFICATION_CHANNEL: + handled = handleNotificationChannelEvent(status, event); + break; + case Type.OTHER: + handled = handleOtherEvent(status, event); + break; + default: + Log.wtf(TAG, "invalid event type " + event.getType() + " for " + event); + } + if (!handled) { + DefaultOmtpEventHandler.handleEvent(context, config, status, event); + } + } + + private static boolean handleConfigurationEvent(Context context, VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case CONFIG_REQUEST_STATUS_SUCCESS: + if (status.getPhoneAccountHandle() == null) { + // This should never happen. + Log.e(TAG, "status editor has null phone account handle"); + return true; + } + + if (!VoicemailChangePinActivity + .isDefaultOldPinSet(context, status.getPhoneAccountHandle())) { + return false; + } else { + postError(status, PIN_NOT_SET); + } + break; + case CONFIG_DEFAULT_PIN_REPLACED: + postError(status, PIN_NOT_SET); + break; + case CONFIG_STATUS_SMS_TIME_OUT: + postError(status, STATUS_SMS_TIMEOUT); + break; + default: + return false; + } + return true; + } + + private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) { + switch (event) { + case DATA_NO_CONNECTION: + case DATA_NO_CONNECTION_CELLULAR_REQUIRED: + case DATA_ALL_SOCKET_CONNECTION_FAILED: + postError(status, VMS_NO_CELLULAR); + break; + case DATA_SSL_INVALID_HOST_NAME: + case DATA_CANNOT_ESTABLISH_SSL_SESSION: + case DATA_IOE_ON_OPEN: + postError(status, VMS_TIMEOUT); + break; + case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK: + postError(status, VMS_DNS_FAILURE); + break; + case DATA_BAD_IMAP_CREDENTIAL: + postError(status, IMAP_ERROR); + break; + case DATA_AUTH_UNKNOWN_USER: + postError(status, UNKNOWN_USER); + break; + case DATA_AUTH_UNKNOWN_DEVICE: + postError(status, UNKNOWN_DEVICE); + break; + case DATA_AUTH_INVALID_PASSWORD: + postError(status, INVALID_PASSWORD); + break; + case DATA_AUTH_MAILBOX_NOT_INITIALIZED: + postError(status, MAILBOX_NOT_INITIALIZED); + break; + case DATA_AUTH_SERVICE_NOT_PROVISIONED: + postError(status, SERVICE_NOT_PROVISIONED); + break; + case DATA_AUTH_SERVICE_NOT_ACTIVATED: + postError(status, SERVICE_NOT_ACTIVATED); + break; + case DATA_AUTH_USER_IS_BLOCKED: + postError(status, USER_BLOCKED); + break; + case DATA_REJECTED_SERVER_RESPONSE: + case DATA_INVALID_INITIAL_SERVER_RESPONSE: + case DATA_SSL_EXCEPTION: + postError(status, IMAP_ERROR); + break; + default: + return false; + } + return true; + } + + private static boolean handleNotificationChannelEvent(VoicemailStatus.Editor status, + OmtpEvents event) { + return false; + } + + private static boolean handleOtherEvent(VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case VVM3_NEW_USER_SETUP_FAILED: + postError(status, MAILBOX_NOT_INITIALIZED); + break; + case VVM3_VMG_DNS_FAILURE: + postError(status, VMG_DNS_FAILURE); + break; + case VVM3_SPG_DNS_FAILURE: + postError(status, SPG_DNS_FAILURE); + break; + case VVM3_VMG_CONNECTION_FAILED: + postError(status, VMG_NO_CELLULAR); + break; + case VVM3_SPG_CONNECTION_FAILED: + postError(status, SPG_NO_CELLULAR); + break; + case VVM3_VMG_TIMEOUT: + postError(status, VMG_TIMEOUT); + break; + case VVM3_SUBSCRIBER_PROVISIONED: + postError(status, SERVICE_NOT_ACTIVATED); + break; + case VVM3_SUBSCRIBER_BLOCKED: + postError(status, SUBSCRIBER_BLOCKED); + break; + case VVM3_SUBSCRIBER_UNKNOWN: + postError(status, SUBSCRIBER_UNKNOWN); + break; + default: + return false; + } + return true; + } + + private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) { + switch (errorCode) { + case VMG_DNS_FAILURE: + case SPG_DNS_FAILURE: + case VMG_NO_CELLULAR: + case SPG_NO_CELLULAR: + case VMG_TIMEOUT: + case SUBSCRIBER_BLOCKED: + case UNKNOWN_USER: + case UNKNOWN_DEVICE: + case INVALID_PASSWORD: + case MAILBOX_NOT_INITIALIZED: + case SERVICE_NOT_PROVISIONED: + case SERVICE_NOT_ACTIVATED: + case USER_BLOCKED: + case VMG_UNKNOWN_ERROR: + case SPG_URL_NOT_FOUND: + case VMG_INTERNAL_ERROR: + case VMG_DB_ERROR: + case VMG_COMMUNICATION_ERROR: + case PIN_NOT_SET: + case SUBSCRIBER_UNKNOWN: + editor.setConfigurationState(errorCode); + break; + case VMS_NO_CELLULAR: + case VMS_DNS_FAILURE: + case VMS_TIMEOUT: + case IMAP_GETQUOTA_ERROR: + case IMAP_SELECT_ERROR: + case IMAP_ERROR: + editor.setDataChannelState(errorCode); + break; + case STATUS_SMS_TIMEOUT: + editor.setNotificationChannelState(errorCode); + break; + default: + Log.wtf(TAG, "unknown error code: " + errorCode); + } + editor.apply(); + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java new file mode 100644 index 000000000..652d1010a --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.Context; +import android.net.Network; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.imap.ImapHelper.InitializingException; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; +import com.android.voicemailomtp.settings.VoicemailChangePinActivity; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.StatusMessage; +import com.android.voicemailomtp.sms.Vvm3MessageSender; +import com.android.voicemailomtp.sync.VvmNetworkRequest; +import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Locale; + +/** + * A flavor of OMTP protocol with a different provisioning process + * + * <p>Used by carriers such as Verizon Wireless + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class Vvm3Protocol extends VisualVoicemailProtocol { + + private static final String TAG = "Vvm3Protocol"; + + private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED"; + private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd"; + private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS"; + private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url"; + + private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; + private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s"; + private static final String IMAP_CLOSE_NUT = "CLOSE_NUT"; + + private static final String ISO639_Spanish = "es"; + + /** + * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link + * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, + * the user can self-provision visual voicemail service. For other response codes, the user must + * contact customer support to resolve the issue. + */ + private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2"; + + // Default prompt level when using the telephone user interface. + // Standard prompt when the user call into the voicemail, and no prompts when someone else is + // leaving a voicemail. + private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5"; + private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6"; + + private static final int DEFAULT_PIN_LENGTH = 6; + + @Override + public void startActivation(OmtpVvmCarrierConfigHelper config, + @Nullable PendingIntent sentIntent) { + // VVM3 does not support activation SMS. + // Send a status request which will start the provisioning process if the user is not + // provisioned. + VvmLog.i(TAG, "Activating"); + config.requestStatus(sentIntent); + } + + @Override + public void startDeactivation(OmtpVvmCarrierConfigHelper config) { + // VVM3 does not support deactivation. + // do nothing. + } + + @Override + public boolean supportsProvisioning() { + return true; + } + + @Override + public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, + Bundle data) { + VvmLog.i(TAG, "start vvm3 provisioning"); + if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "Provisioning status: Unknown"); + if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE + .equals(message.getReturnCode())) { + VvmLog.i(TAG, "Self provisioning available, subscribing"); + new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe(); + } else { + config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN); + } + } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "setting up new user"); + // Save the IMAP credentials in preferences so they are persistent and can be retrieved. + VisualVoicemailPreferences prefs = + new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle); + message.putStatus(prefs.edit()).apply(); + + startProvisionNewUser(task, phoneAccountHandle, config, status, message); + } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "User provisioned but not activated, disabling VVM"); + VisualVoicemailSettingsUtil + .setEnabled(config.getContext(), phoneAccountHandle, false); + } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "User blocked"); + config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED); + } + } + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber); + } + + @Override + public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + Vvm3EventHandler.handleEvent(context, config, status, event); + } + + @Override + public String getCommand(String command) { + if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) { + return IMAP_CHANGE_TUI_PWD_FORMAT; + } + if (command == OmtpConstants.IMAP_CLOSE_NUT) { + return IMAP_CLOSE_NUT; + } + if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) { + return IMAP_CHANGE_VM_LANG_FORMAT; + } + return super.getCommand(command); + } + + @Override + public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, + Bundle data) { + // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned + // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status + // so provisioning can be done. + if (!SMS_EVENT_UNRECOGNIZED.equals(event)) { + return null; + } + if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) { + return null; + } + Bundle bundle = new Bundle(); + bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN); + bundle.putString(OmtpConstants.RETURN_CODE, + VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE); + String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY); + if (TextUtils.isEmpty(vmgUrl)) { + VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config"); + return null; + } + bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl); + VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS"); + return bundle; + } + + private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, + StatusMessage message) { + try (NetworkWrapper wrapper = VvmNetworkRequest + .getNetwork(config, phoneAccountHandle, status)) { + Network network = wrapper.get(); + + VvmLog.i(TAG, "new user: network available"); + try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle, + network, status)) { + // VVM3 has inconsistent error language code to OMTP. Just issue a raw command + // here. + // TODO(b/29082671): use LocaleList + if (Locale.getDefault().getLanguage() + .equals(new Locale(ISO639_Spanish).getLanguage())) { + // Spanish + helper.changeVoicemailTuiLanguage( + VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS); + } else { + // English + helper.changeVoicemailTuiLanguage( + VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS); + } + VvmLog.i(TAG, "new user: language set"); + + if (setPin(config.getContext(), phoneAccountHandle, helper, message)) { + // Only close new user tutorial if the PIN has been changed. + helper.closeNewUserTutorial(); + VvmLog.i(TAG, "new user: NUT closed"); + + config.requestStatus(null); + } + } catch (InitializingException | MessagingException | IOException e) { + config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED); + task.fail(); + VvmLog.e(TAG, e.toString()); + } + } catch (RequestFailedException e) { + config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + task.fail(); + } + + } + + + private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle, + ImapHelper helper, StatusMessage message) + throws IOException, MessagingException { + String defaultPin = getDefaultPin(message); + if (defaultPin == null) { + VvmLog.i(TAG, "cannot generate default PIN"); + return false; + } + + if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) { + // The pin was already set + VvmLog.i(TAG, "PIN already set"); + return true; + } + String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle)); + if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) { + VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin); + helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED); + } + VvmLog.i(TAG, "new user: PIN set"); + return true; + } + + @Nullable + private static String getDefaultPin(StatusMessage message) { + // The IMAP username is [phone number]@example.com + String username = message.getImapUserName(); + try { + String number = username.substring(0, username.indexOf('@')); + if (number.length() < 4) { + VvmLog.e(TAG, "unable to extract number from IMAP username"); + return null; + } + return "1" + number.substring(number.length() - 4); + } catch (StringIndexOutOfBoundsException e) { + VvmLog.e(TAG, "unable to extract number from IMAP username"); + return null; + } + + } + + private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) { + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, + phoneAccountHandle); + // The OMTP pin length format is {min}-{max} + String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); + if (lengths.length == 2) { + try { + return Integer.parseInt(lengths[0]); + } catch (NumberFormatException e) { + return DEFAULT_PIN_LENGTH; + } + } + return DEFAULT_PIN_LENGTH; + } + + private static String generatePin(int length) { + SecureRandom random = new SecureRandom(); + return String.format(Locale.US, "%010d", Math.abs(random.nextLong())) + .substring(0, length); + + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java new file mode 100644 index 000000000..0a4d792b2 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.voicemailomtp.protocol; + +import android.annotation.TargetApi; +import android.net.Network; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.Html; +import android.text.Spanned; +import android.text.style.URLSpan; +import android.util.ArrayMap; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.sync.VvmNetworkRequest; +import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException; +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.HurlStack; +import com.android.volley.toolbox.RequestFuture; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.toolbox.Volley; +import java.io.IOException; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required + * when the user is unprovisioned. This could happen when the user is on a legacy service, or + * switched over from devices that used other type of visual voicemail. + * + * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find + * the self provisioning gateway URL that we can modify voicemail services. + * + * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us + * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the + * subscription. This link should be clicked through cellular network, and have cookies enabled. + * + * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or + * ready user. + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class Vvm3Subscriber { + + private static final String TAG = "Vvm3Subscriber"; + + private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL"; + private static final String SPG_URL_TAG = "spgurl"; + private static final String TRANSACTION_ID_TAG = "transactionid"; + //language=XML + private static final String VMG_XML_REQUEST_FORMAT = "" + + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + + "<VMGVVMRequest>" + + " <MessageHeader>" + + " <transactionid>%1$s</transactionid>" + + " </MessageHeader>" + + " <MessageBody>" + + " <mdn>%2$s</mdn>" + + " <operation>%3$s</operation>" + + " <source>Device</source>" + + " <devicemodel>%4$s</devicemodel>" + + " </MessageBody>" + + "</VMGVVMRequest>"; + + static final String VMG_URL_KEY = "vmg_url"; + + // Self provisioning POST key/values. VVM3 API 2.1.0 12.3 + private static final String SPG_VZW_MDN_PARAM = "VZW_MDN"; + private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE"; + private static final String SPG_VZW_SERVICE_BASIC = "BVVM"; + private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL"; + // Value for all android device + private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G"; + private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN"; + private static final String SPG_APP_TOKEN = "q8e3t5u2o1"; + private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM"; + private static final String SPG_LANGUAGE_EN = "ENGLISH"; + + private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail"; + + private static final int REQUEST_TIMEOUT_SECONDS = 30; + + private final ActivationTask mTask; + private final PhoneAccountHandle mHandle; + private final OmtpVvmCarrierConfigHelper mHelper; + private final VoicemailStatus.Editor mStatus; + private final Bundle mData; + + private final String mNumber; + + private RequestQueue mRequestQueue; + + private static class ProvisioningException extends Exception { + + public ProvisioningException(String message) { + super(message); + } + } + + static { + // Set the default cookie handler to retain session data for the self provisioning gateway. + // Note; this is not ideal as it is application-wide, and can easily get clobbered. + // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually + // managing cookies will greatly increase complexity. + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + } + + @WorkerThread + public Vvm3Subscriber(ActivationTask task, PhoneAccountHandle handle, + OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data) { + Assert.isNotMainThread(); + mTask = task; + mHandle = handle; + mHelper = helper; + mStatus = status; + mData = data; + + // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username + // is not included in the status SMS, thus no other way to get the current phone number. + mNumber = mHelper.getContext().getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(mHandle).getLine1Number(); + } + + @WorkerThread + public void subscribe() { + Assert.isNotMainThread(); + // Cellular data is required to subscribe. + // processSubscription() is called after network is available. + VvmLog.i(TAG, "Subscribing"); + + try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) { + Network network = wrapper.get(); + VvmLog.d(TAG, "provisioning: network available"); + mRequestQueue = Volley + .newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network)); + processSubscription(); + } catch (RequestFailedException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED); + mTask.fail(); + } + } + + private void processSubscription() { + try { + String gatewayUrl = getSelfProvisioningGateway(); + String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl); + String subscribeLink = findSubscribeLink(selfProvisionResponse); + clickSubscribeLink(subscribeLink); + } catch (ProvisioningException e) { + VvmLog.e(TAG, e.toString()); + mTask.fail(); + } + } + + /** + * Get the URL to perform self-provisioning from the voicemail management gateway. + */ + private String getSelfProvisioningGateway() throws ProvisioningException { + VvmLog.i(TAG, "retrieving SPG URL"); + String response = vvm3XmlRequest(OPERATION_GET_SPG_URL); + return extractText(response, SPG_URL_TAG); + } + + /** + * Sent a request to the self-provisioning gateway, which will return us with a webpage. The + * page might contain a "Subscribe to Basic Visual Voice Mail" link to complete the + * subscription. The cookie from this response and cellular data is required to click the link. + */ + private String getSelfProvisionResponse(String url) throws ProvisioningException { + VvmLog.i(TAG, "Retrieving self provisioning response"); + + RequestFuture<String> future = RequestFuture.newFuture(); + + StringRequest stringRequest = new StringRequest(Request.Method.POST, url, future, future) { + @Override + protected Map<String, String> getParams() { + Map<String, String> params = new ArrayMap<>(); + params.put(SPG_VZW_MDN_PARAM, mNumber); + params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC); + params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID); + params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN); + // Language to display the subscription page. The page is never shown to the user + // so just use English. + params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN); + return params; + } + }; + + mRequestQueue.add(stringRequest); + try { + return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + } + + private void clickSubscribeLink(String subscribeLink) throws ProvisioningException { + VvmLog.i(TAG, "Clicking subscribe link"); + RequestFuture<String> future = RequestFuture.newFuture(); + + StringRequest stringRequest = new StringRequest(Request.Method.POST, + subscribeLink, future, future); + mRequestQueue.add(stringRequest); + try { + // A new STATUS SMS will be sent after this request. + future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException | ExecutionException | InterruptedException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + // It could take very long for the STATUS SMS to return. Waiting for it is unreliable. + // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always + // manually retry if it took too long. + } + + private String vvm3XmlRequest(String operation) throws ProvisioningException { + VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation); + String voicemailManagementGateway = mData.getString(VMG_URL_KEY); + if (voicemailManagementGateway == null) { + VvmLog.e(TAG, "voicemailManagementGateway url unknown"); + return null; + } + String transactionId = createTransactionId(); + String body = String.format(Locale.US, VMG_XML_REQUEST_FORMAT, + transactionId, mNumber, operation, Build.MODEL); + + RequestFuture<String> future = RequestFuture.newFuture(); + StringRequest stringRequest = new StringRequest(Request.Method.POST, + voicemailManagementGateway, future, future) { + @Override + public byte[] getBody() throws AuthFailureError { + return body.getBytes(); + } + }; + mRequestQueue.add(stringRequest); + + try { + String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) { + throw new ProvisioningException("transactionId mismatch"); + } + return response; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + } + + private String findSubscribeLink(String response) throws ProvisioningException { + Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY); + URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class); + StringBuilder fulltext = new StringBuilder(); + for (URLSpan span : spans) { + String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString(); + if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) { + return span.getURL(); + } + fulltext.append(text); + } + throw new ProvisioningException("Subscribe link not found: " + fulltext); + } + + private String createTransactionId() { + return String.valueOf(Math.abs(new Random().nextLong())); + } + + private String extractText(String xml, String tag) throws ProvisioningException { + Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">"); + Matcher matcher = pattern.matcher(xml); + if (matcher.find()) { + return matcher.group(1); + } + throw new ProvisioningException("Tag " + tag + " not found in xml response"); + } + + private static class NetworkSpecifiedHurlStack extends HurlStack { + + private final Network mNetwork; + + public NetworkSpecifiedHurlStack(Network network) { + mNetwork = network; + } + + @Override + protected HttpURLConnection createConnection(URL url) throws IOException { + return (HttpURLConnection) mNetwork.openConnection(url); + } + + } +} |