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.phone.vvm.omtp.protocol;
     18 
     19 import android.annotation.Nullable;
     20 import android.app.PendingIntent;
     21 import android.content.Context;
     22 import android.net.Network;
     23 import android.os.Bundle;
     24 import android.telecom.PhoneAccountHandle;
     25 import android.telephony.SmsManager;
     26 import android.text.TextUtils;
     27 
     28 import com.android.phone.PhoneGlobals;
     29 import com.android.phone.VoicemailStatus;
     30 import com.android.phone.common.mail.MessagingException;
     31 import com.android.phone.settings.VisualVoicemailSettingsUtil;
     32 import com.android.phone.settings.VoicemailChangePinActivity;
     33 import com.android.phone.vvm.omtp.ActivationTask;
     34 import com.android.phone.vvm.omtp.OmtpConstants;
     35 import com.android.phone.vvm.omtp.OmtpEvents;
     36 import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
     37 import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
     38 import com.android.phone.vvm.omtp.VvmLog;
     39 import com.android.phone.vvm.omtp.imap.ImapHelper;
     40 import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException;
     41 import com.android.phone.vvm.omtp.sms.OmtpMessageSender;
     42 import com.android.phone.vvm.omtp.sms.StatusMessage;
     43 import com.android.phone.vvm.omtp.sms.Vvm3MessageSender;
     44 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest;
     45 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.NetworkWrapper;
     46 import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.RequestFailedException;
     47 
     48 import java.io.IOException;
     49 import java.security.SecureRandom;
     50 import 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  */
     57 public 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