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;
27
28import com.android.phone.PhoneGlobals;
29import com.android.phone.VoicemailStatus;
30import com.android.phone.common.mail.MessagingException;
31import com.android.phone.settings.VisualVoicemailSettingsUtil;
32import com.android.phone.settings.VoicemailChangePinActivity;
33import com.android.phone.vvm.omtp.ActivationTask;
34import com.android.phone.vvm.omtp.OmtpConstants;
35import com.android.phone.vvm.omtp.OmtpEvents;
36import com.android.phone.vvm.omtp.OmtpVvmCarrierConfigHelper;
37import com.android.phone.vvm.omtp.VisualVoicemailPreferences;
38import com.android.phone.vvm.omtp.VvmLog;
39import com.android.phone.vvm.omtp.imap.ImapHelper;
40import com.android.phone.vvm.omtp.imap.ImapHelper.InitializingException;
41import com.android.phone.vvm.omtp.sms.OmtpMessageSender;
42import com.android.phone.vvm.omtp.sms.StatusMessage;
43import com.android.phone.vvm.omtp.sms.Vvm3MessageSender;
44import com.android.phone.vvm.omtp.sync.VvmNetworkRequest;
45import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.NetworkWrapper;
46import com.android.phone.vvm.omtp.sync.VvmNetworkRequest.RequestFailedException;
47
48import java.io.IOException;
49import java.security.SecureRandom;
50import 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 */
57public 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