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