Home | History | Annotate | Download | only in protocol
      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