326 lines
13 KiB
Java
326 lines
13 KiB
Java
|
/*
|
||
|
* 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.voicemail.impl.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.dialer.logging.DialerImpression;
|
||
|
import com.android.voicemail.PinChanger;
|
||
|
import com.android.voicemail.VoicemailComponent;
|
||
|
import com.android.voicemail.impl.ActivationTask;
|
||
|
import com.android.voicemail.impl.OmtpConstants;
|
||
|
import com.android.voicemail.impl.OmtpEvents;
|
||
|
import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
|
||
|
import com.android.voicemail.impl.VisualVoicemailPreferences;
|
||
|
import com.android.voicemail.impl.VoicemailStatus;
|
||
|
import com.android.voicemail.impl.VvmLog;
|
||
|
import com.android.voicemail.impl.imap.ImapHelper;
|
||
|
import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
|
||
|
import com.android.voicemail.impl.mail.MessagingException;
|
||
|
import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
|
||
|
import com.android.voicemail.impl.sms.OmtpMessageSender;
|
||
|
import com.android.voicemail.impl.sms.StatusMessage;
|
||
|
import com.android.voicemail.impl.sms.Vvm3MessageSender;
|
||
|
import com.android.voicemail.impl.sync.VvmNetworkRequest;
|
||
|
import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
|
||
|
import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
|
||
|
import com.android.voicemail.impl.utils.LoggerUtils;
|
||
|
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.O)
|
||
|
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,
|
||
|
boolean isCarrierInitiated) {
|
||
|
VvmLog.i(TAG, "start vvm3 provisioning");
|
||
|
|
||
|
if (isCarrierInitiated) {
|
||
|
// Carrier can send the "Status UNKNOWN, Can subscribe" status when upgrading to premium VVM.
|
||
|
// Ignore so we won't downgrade it back to basic.
|
||
|
VvmLog.w(TAG, "carrier initiated, ignoring");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
config.getContext(), DialerImpression.Type.VVM_PROVISIONING_STARTED);
|
||
|
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) {
|
||
|
switch (command) {
|
||
|
case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT:
|
||
|
return IMAP_CHANGE_TUI_PWD_FORMAT;
|
||
|
case OmtpConstants.IMAP_CLOSE_NUT:
|
||
|
return IMAP_CLOSE_NUT;
|
||
|
case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT:
|
||
|
return IMAP_CHANGE_VM_LANG_FORMAT;
|
||
|
default:
|
||
|
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(a bug): 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");
|
||
|
LoggerUtils.logImpressionOnMainThread(
|
||
|
config.getContext(), DialerImpression.Type.VVM_PROVISIONING_COMPLETED);
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
PinChanger pinChanger =
|
||
|
VoicemailComponent.get(context)
|
||
|
.getVoicemailClient()
|
||
|
.createPinChanger(context, phoneAccountHandle);
|
||
|
|
||
|
if (pinChanger.getScrambledPin() != null) {
|
||
|
// The pin was already set
|
||
|
VvmLog.i(TAG, "PIN already set");
|
||
|
return true;
|
||
|
}
|
||
|
String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
|
||
|
if (helper.changePin(defaultPin, newPin) == PinChanger.CHANGE_PIN_SUCCESS) {
|
||
|
pinChanger.setScrambledPin(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);
|
||
|
}
|
||
|
}
|