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