Vvm3Protocol.java revision 9a090c89e7a0c43a668bd6e2ac5e5e680f323aed
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.Logger;
29import com.android.dialer.logging.nano.DialerImpression;
30import com.android.voicemail.impl.ActivationTask;
31import com.android.voicemail.impl.OmtpConstants;
32import com.android.voicemail.impl.OmtpEvents;
33import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
34import com.android.voicemail.impl.VisualVoicemailPreferences;
35import com.android.voicemail.impl.VoicemailStatus;
36import com.android.voicemail.impl.VvmLog;
37import com.android.voicemail.impl.imap.ImapHelper;
38import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
39import com.android.voicemail.impl.mail.MessagingException;
40import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
41import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
42import com.android.voicemail.impl.sms.OmtpMessageSender;
43import com.android.voicemail.impl.sms.StatusMessage;
44import com.android.voicemail.impl.sms.Vvm3MessageSender;
45import com.android.voicemail.impl.sync.VvmNetworkRequest;
46import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
47import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
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    Logger.get(config.getContext()).logImpression(DialerImpression.Type.VVM_PROVISIONING_STARTED);
120    if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
121      VvmLog.i(TAG, "Provisioning status: Unknown");
122      if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE.equals(message.getReturnCode())) {
123        VvmLog.i(TAG, "Self provisioning available, subscribing");
124        new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
125      } else {
126        config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
127      }
128    } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
129      VvmLog.i(TAG, "setting up new user");
130      // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
131      VisualVoicemailPreferences prefs =
132          new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
133      message.putStatus(prefs.edit()).apply();
134
135      startProvisionNewUser(task, phoneAccountHandle, config, status, message);
136    } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
137      VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
138      VisualVoicemailSettingsUtil.setEnabled(config.getContext(), phoneAccountHandle, 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    }
143  }
144
145  @Override
146  public OmtpMessageSender createMessageSender(
147      Context context,
148      PhoneAccountHandle phoneAccountHandle,
149      short applicationPort,
150      String destinationNumber) {
151    return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, destinationNumber);
152  }
153
154  @Override
155  public void handleEvent(
156      Context context,
157      OmtpVvmCarrierConfigHelper config,
158      VoicemailStatus.Editor status,
159      OmtpEvents event) {
160    Vvm3EventHandler.handleEvent(context, config, status, event);
161  }
162
163  @Override
164  public String getCommand(String command) {
165    switch (command) {
166      case OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT:
167        return IMAP_CHANGE_TUI_PWD_FORMAT;
168      case OmtpConstants.IMAP_CLOSE_NUT:
169        return IMAP_CLOSE_NUT;
170      case OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT:
171        return IMAP_CHANGE_VM_LANG_FORMAT;
172      default:
173        return super.getCommand(command);
174    }
175  }
176
177  @Override
178  public Bundle translateStatusSmsBundle(
179      OmtpVvmCarrierConfigHelper config, String event, Bundle data) {
180    // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
181    // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
182    // so provisioning can be done.
183    if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
184      return null;
185    }
186    if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
187      return null;
188    }
189    Bundle bundle = new Bundle();
190    bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
191    bundle.putString(
192        OmtpConstants.RETURN_CODE, VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
193    String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
194    if (TextUtils.isEmpty(vmgUrl)) {
195      VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
196      return null;
197    }
198    bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
199    VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
200    return bundle;
201  }
202
203  private void startProvisionNewUser(
204      ActivationTask task,
205      PhoneAccountHandle phoneAccountHandle,
206      OmtpVvmCarrierConfigHelper config,
207      VoicemailStatus.Editor status,
208      StatusMessage message) {
209    try (NetworkWrapper wrapper =
210        VvmNetworkRequest.getNetwork(config, phoneAccountHandle, status)) {
211      Network network = wrapper.get();
212
213      VvmLog.i(TAG, "new user: network available");
214      try (ImapHelper helper =
215          new ImapHelper(config.getContext(), phoneAccountHandle, network, status)) {
216        // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
217        // here.
218        // TODO(b/29082671): use LocaleList
219        if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_Spanish).getLanguage())) {
220          // Spanish
221          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
222        } else {
223          // English
224          helper.changeVoicemailTuiLanguage(VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
225        }
226        VvmLog.i(TAG, "new user: language set");
227
228        if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
229          // Only close new user tutorial if the PIN has been changed.
230          helper.closeNewUserTutorial();
231          VvmLog.i(TAG, "new user: NUT closed");
232          Logger.get(config.getContext())
233              .logImpression(DialerImpression.Type.VVM_PROVISIONING_COMPLETED);
234          config.requestStatus(null);
235        }
236      } catch (InitializingException | MessagingException | IOException e) {
237        config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
238        task.fail();
239        VvmLog.e(TAG, e.toString());
240      }
241    } catch (RequestFailedException e) {
242      config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
243      task.fail();
244    }
245  }
246
247  private static boolean setPin(
248      Context context,
249      PhoneAccountHandle phoneAccountHandle,
250      ImapHelper helper,
251      StatusMessage message)
252      throws IOException, MessagingException {
253    String defaultPin = getDefaultPin(message);
254    if (defaultPin == null) {
255      VvmLog.i(TAG, "cannot generate default PIN");
256      return false;
257    }
258
259    if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
260      // The pin was already set
261      VvmLog.i(TAG, "PIN already set");
262      return true;
263    }
264    String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
265    if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
266      VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
267      helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
268    }
269    VvmLog.i(TAG, "new user: PIN set");
270    return true;
271  }
272
273  @Nullable
274  private static String getDefaultPin(StatusMessage message) {
275    // The IMAP username is [phone number]@example.com
276    String username = message.getImapUserName();
277    try {
278      String number = username.substring(0, username.indexOf('@'));
279      if (number.length() < 4) {
280        VvmLog.e(TAG, "unable to extract number from IMAP username");
281        return null;
282      }
283      return "1" + number.substring(number.length() - 4);
284    } catch (StringIndexOutOfBoundsException e) {
285      VvmLog.e(TAG, "unable to extract number from IMAP username");
286      return null;
287    }
288  }
289
290  private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
291    VisualVoicemailPreferences preferences =
292        new VisualVoicemailPreferences(context, phoneAccountHandle);
293    // The OMTP pin length format is {min}-{max}
294    String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
295    if (lengths.length == 2) {
296      try {
297        return Integer.parseInt(lengths[0]);
298      } catch (NumberFormatException e) {
299        return DEFAULT_PIN_LENGTH;
300      }
301    }
302    return DEFAULT_PIN_LENGTH;
303  }
304
305  private static String generatePin(int length) {
306    SecureRandom random = new SecureRandom();
307    return String.format(Locale.US, "%010d", Math.abs(random.nextLong())).substring(0, length);
308  }
309}
310