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