• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.voicemail.impl.protocol;
18 
19 import android.annotation.TargetApi;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.net.Network;
23 import android.os.Build.VERSION_CODES;
24 import android.os.Bundle;
25 import android.support.annotation.Nullable;
26 import android.telecom.PhoneAccountHandle;
27 import android.text.TextUtils;
28 import com.android.dialer.logging.DialerImpression;
29 import com.android.voicemail.impl.ActivationTask;
30 import com.android.voicemail.impl.OmtpConstants;
31 import com.android.voicemail.impl.OmtpEvents;
32 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
33 import com.android.voicemail.impl.VisualVoicemailPreferences;
34 import com.android.voicemail.impl.VoicemailStatus;
35 import com.android.voicemail.impl.VvmLog;
36 import com.android.voicemail.impl.imap.ImapHelper;
37 import com.android.voicemail.impl.imap.ImapHelper.InitializingException;
38 import com.android.voicemail.impl.mail.MessagingException;
39 import com.android.voicemail.impl.settings.VisualVoicemailSettingsUtil;
40 import com.android.voicemail.impl.settings.VoicemailChangePinActivity;
41 import com.android.voicemail.impl.sms.OmtpMessageSender;
42 import com.android.voicemail.impl.sms.StatusMessage;
43 import com.android.voicemail.impl.sms.Vvm3MessageSender;
44 import com.android.voicemail.impl.sync.VvmNetworkRequest;
45 import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
46 import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
47 import com.android.voicemail.impl.utils.LoggerUtils;
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  * <p>Used by carriers such as Verizon Wireless
56  */
57 @TargetApi(VERSION_CODES.O)
58 public 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
startActivation( OmtpVvmCarrierConfigHelper config, @Nullable PendingIntent sentIntent)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
startDeactivation(OmtpVvmCarrierConfigHelper config)100   public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
101     // VVM3 does not support deactivation.
102     // do nothing.
103   }
104 
105   @Override
supportsProvisioning()106   public boolean supportsProvisioning() {
107     return true;
108   }
109 
110   @Override
startProvisioning( ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, Bundle data)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
createMessageSender( Context context, PhoneAccountHandle phoneAccountHandle, short applicationPort, String destinationNumber)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
handleEvent( Context context, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event)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
getCommand(String command)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
translateStatusSmsBundle( OmtpVvmCarrierConfigHelper config, String event, Bundle data)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 
startProvisionNewUser( ActivationTask task, PhoneAccountHandle phoneAccountHandle, OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message)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 
setPin( Context context, PhoneAccountHandle phoneAccountHandle, ImapHelper helper, StatusMessage message)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
getDefaultPin(StatusMessage message)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 
getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle)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 
generatePin(int length)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