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.content.Context;
21 import android.net.Network;
22 import android.os.Build;
23 import android.os.Build.VERSION_CODES;
24 import android.os.Bundle;
25 import android.support.annotation.NonNull;
26 import android.support.annotation.VisibleForTesting;
27 import android.support.annotation.WorkerThread;
28 import android.telecom.PhoneAccountHandle;
29 import android.telephony.TelephonyManager;
30 import android.text.Html;
31 import android.text.Spanned;
32 import android.text.style.URLSpan;
33 import android.util.ArrayMap;
34 import com.android.dialer.configprovider.ConfigProviderComponent;
35 import com.android.voicemail.impl.ActivationTask;
36 import com.android.voicemail.impl.Assert;
37 import com.android.voicemail.impl.OmtpEvents;
38 import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
39 import com.android.voicemail.impl.VoicemailStatus;
40 import com.android.voicemail.impl.VvmLog;
41 import com.android.voicemail.impl.sync.VvmNetworkRequest;
42 import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
43 import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
44 import com.android.volley.AuthFailureError;
45 import com.android.volley.Request;
46 import com.android.volley.RequestQueue;
47 import com.android.volley.toolbox.HurlStack;
48 import com.android.volley.toolbox.RequestFuture;
49 import com.android.volley.toolbox.StringRequest;
50 import com.android.volley.toolbox.Volley;
51 import java.io.IOException;
52 import java.net.CookieHandler;
53 import java.net.CookieManager;
54 import java.net.HttpURLConnection;
55 import java.net.URL;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Random;
61 import java.util.concurrent.ExecutionException;
62 import java.util.concurrent.TimeUnit;
63 import java.util.concurrent.TimeoutException;
64 import java.util.regex.Matcher;
65 import java.util.regex.Pattern;
66 import org.json.JSONArray;
67 import org.json.JSONException;
68 
69 /**
70  * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
71  * when the user is unprovisioned. This could happen when the user is on a legacy service, or
72  * switched over from devices that used other type of visual voicemail.
73  *
74  * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
75  * the self provisioning gateway URL that we can modify voicemail services.
76  *
77  * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
78  * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
79  * subscription. This link should be clicked through cellular network, and have cookies enabled.
80  *
81  * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
82  * ready user.
83  */
84 @TargetApi(VERSION_CODES.O)
85 public class Vvm3Subscriber {
86 
87   private static final String TAG = "Vvm3Subscriber";
88 
89   private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
90   private static final String SPG_URL_TAG = "spgurl";
91   private static final String TRANSACTION_ID_TAG = "transactionid";
92   // language=XML
93   private static final String VMG_XML_REQUEST_FORMAT =
94       ""
95           + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
96           + "<VMGVVMRequest>"
97           + "  <MessageHeader>"
98           + "    <transactionid>%1$s</transactionid>"
99           + "  </MessageHeader>"
100           + "  <MessageBody>"
101           + "    <mdn>%2$s</mdn>"
102           + "    <operation>%3$s</operation>"
103           + "    <source>Device</source>"
104           + "    <devicemodel>%4$s</devicemodel>"
105           + "  </MessageBody>"
106           + "</VMGVVMRequest>";
107 
108   static final String VMG_URL_KEY = "vmg_url";
109 
110   // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
111   private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
112   private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
113   private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
114   private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
115   // Value for all android device
116   private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
117   private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
118   private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
119   private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
120   private static final String SPG_LANGUAGE_EN = "ENGLISH";
121 
122   @VisibleForTesting
123   static final String VVM3_SUBSCRIBE_LINK_PATTERNS_JSON_ARRAY =
124       "vvm3_subscribe_link_pattern_json_array";
125 
126   private static final String VVM3_SUBSCRIBE_LINK_DEFAULT_PATTERNS =
127       "["
128           + "\"(?i)Subscribe to Basic Visual Voice Mail\","
129           + "\"(?i)Subscribe to Basic Visual Voicemail\""
130           + "]";
131 
132   private static final int REQUEST_TIMEOUT_SECONDS = 30;
133 
134   private final ActivationTask task;
135   private final PhoneAccountHandle handle;
136   private final OmtpVvmCarrierConfigHelper helper;
137   private final VoicemailStatus.Editor status;
138   private final Bundle data;
139 
140   private final String number;
141 
142   private RequestQueue requestQueue;
143 
144   @VisibleForTesting
145   static class ProvisioningException extends Exception {
146 
ProvisioningException(String message)147     public ProvisioningException(String message) {
148       super(message);
149     }
150   }
151 
152   static {
153     // Set the default cookie handler to retain session data for the self provisioning gateway.
154     // Note; this is not ideal as it is application-wide, and can easily get clobbered.
155     // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
156     // managing cookies will greatly increase complexity.
157     CookieManager cookieManager = new CookieManager();
158     CookieHandler.setDefault(cookieManager);
159   }
160 
161   @WorkerThread
162   @SuppressWarnings("missingPermission")
Vvm3Subscriber( ActivationTask task, PhoneAccountHandle handle, OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data)163   public Vvm3Subscriber(
164       ActivationTask task,
165       PhoneAccountHandle handle,
166       OmtpVvmCarrierConfigHelper helper,
167       VoicemailStatus.Editor status,
168       Bundle data) {
169     Assert.isNotMainThread();
170     this.task = task;
171     this.handle = handle;
172     this.helper = helper;
173     this.status = status;
174     this.data = data;
175 
176     // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
177     // is not included in the status SMS, thus no other way to get the current phone number.
178     number =
179         stripInternational(
180             this.helper
181                 .getContext()
182                 .getSystemService(TelephonyManager.class)
183                 .createForPhoneAccountHandle(this.handle)
184                 .getLine1Number());
185   }
186 
187   /**
188    * Self provisioning gateway expects 10 digit national format, but {@link
189    * TelephonyManager#getLine1Number()} might return e164 with "+1" at front.
190    */
stripInternational(String number)191   private static String stripInternational(String number) {
192     if (number.startsWith("+1")) {
193       number = number.substring(2);
194     }
195     return number;
196   }
197 
198   @WorkerThread
subscribe()199   public void subscribe() {
200     Assert.isNotMainThread();
201     // Cellular data is required to subscribe.
202     // processSubscription() is called after network is available.
203     VvmLog.i(TAG, "Subscribing");
204 
205     try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(helper, handle, status)) {
206       Network network = wrapper.get();
207       VvmLog.d(TAG, "provisioning: network available");
208       requestQueue =
209           Volley.newRequestQueue(helper.getContext(), new NetworkSpecifiedHurlStack(network));
210       processSubscription();
211     } catch (RequestFailedException e) {
212       helper.handleEvent(status, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
213       task.fail();
214     }
215   }
216 
processSubscription()217   private void processSubscription() {
218     try {
219       String gatewayUrl = getSelfProvisioningGateway();
220       String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
221       String subscribeLink =
222           findSubscribeLink(getSubscribeLinkPatterns(helper.getContext()), selfProvisionResponse);
223       clickSubscribeLink(subscribeLink);
224     } catch (ProvisioningException e) {
225       VvmLog.e(TAG, e.toString());
226       helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
227       task.fail();
228     }
229   }
230 
231   /** Get the URL to perform self-provisioning from the voicemail management gateway. */
getSelfProvisioningGateway()232   private String getSelfProvisioningGateway() throws ProvisioningException {
233     VvmLog.i(TAG, "retrieving SPG URL");
234     String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
235     return extractText(response, SPG_URL_TAG);
236   }
237 
238   /**
239    * Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
240    * might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
241    * cookie from this response and cellular data is required to click the link.
242    */
getSelfProvisionResponse(String url)243   private String getSelfProvisionResponse(String url) throws ProvisioningException {
244     VvmLog.i(TAG, "Retrieving self provisioning response");
245 
246     RequestFuture<String> future = RequestFuture.newFuture();
247 
248     StringRequest stringRequest =
249         new StringRequest(Request.Method.POST, url, future, future) {
250           @Override
251           protected Map<String, String> getParams() {
252             Map<String, String> params = new ArrayMap<>();
253             params.put(SPG_VZW_MDN_PARAM, number);
254             params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
255             params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
256             params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
257             // Language to display the subscription page. The page is never shown to the user
258             // so just use English.
259             params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
260             return params;
261           }
262         };
263 
264     requestQueue.add(stringRequest);
265     try {
266       return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
267     } catch (InterruptedException | ExecutionException | TimeoutException e) {
268       helper.handleEvent(status, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
269       throw new ProvisioningException(e.toString());
270     }
271   }
272 
clickSubscribeLink(String subscribeLink)273   private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
274     VvmLog.i(TAG, "Clicking subscribe link");
275     RequestFuture<String> future = RequestFuture.newFuture();
276 
277     StringRequest stringRequest =
278         new StringRequest(Request.Method.POST, subscribeLink, future, future);
279     requestQueue.add(stringRequest);
280     try {
281       // A new STATUS SMS will be sent after this request.
282       future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
283     } catch (TimeoutException | ExecutionException | InterruptedException e) {
284       helper.handleEvent(status, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
285       throw new ProvisioningException(e.toString());
286     }
287     // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
288     // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
289     // manually retry if it took too long.
290   }
291 
vvm3XmlRequest(String operation)292   private String vvm3XmlRequest(String operation) throws ProvisioningException {
293     VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
294     String voicemailManagementGateway = data.getString(VMG_URL_KEY);
295     if (voicemailManagementGateway == null) {
296       VvmLog.e(TAG, "voicemailManagementGateway url unknown");
297       return null;
298     }
299     String transactionId = createTransactionId();
300     String body =
301         String.format(
302             Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, number, operation, Build.MODEL);
303 
304     RequestFuture<String> future = RequestFuture.newFuture();
305     StringRequest stringRequest =
306         new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
307           @Override
308           public byte[] getBody() throws AuthFailureError {
309             return body.getBytes();
310           }
311         };
312     requestQueue.add(stringRequest);
313 
314     try {
315       String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
316       if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
317         throw new ProvisioningException("transactionId mismatch");
318       }
319       return response;
320     } catch (InterruptedException | ExecutionException | TimeoutException e) {
321       helper.handleEvent(status, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
322       throw new ProvisioningException(e.toString());
323     }
324   }
325 
326   @VisibleForTesting
getSubscribeLinkPatterns(Context context)327   static List<Pattern> getSubscribeLinkPatterns(Context context) {
328     String patternsJsonString =
329         ConfigProviderComponent.get(context)
330             .getConfigProvider()
331             .getString(
332                 VVM3_SUBSCRIBE_LINK_PATTERNS_JSON_ARRAY, VVM3_SUBSCRIBE_LINK_DEFAULT_PATTERNS);
333     List<Pattern> patterns = new ArrayList<>();
334     try {
335       JSONArray patternsArray = new JSONArray(patternsJsonString);
336       for (int i = 0; i < patternsArray.length(); i++) {
337         patterns.add(Pattern.compile(patternsArray.getString(i)));
338       }
339     } catch (JSONException e) {
340       throw new IllegalArgumentException("Unable to parse patterns" + e);
341     }
342     return patterns;
343   }
344 
345   @VisibleForTesting
findSubscribeLink(@onNull List<Pattern> patterns, String response)346   static String findSubscribeLink(@NonNull List<Pattern> patterns, String response)
347       throws ProvisioningException {
348     if (patterns.isEmpty()) {
349       throw new IllegalArgumentException("empty patterns");
350     }
351     Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
352     URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
353     StringBuilder fulltext = new StringBuilder();
354 
355     for (URLSpan span : spans) {
356       String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
357       for (Pattern pattern : patterns) {
358         if (pattern.matcher(text).matches()) {
359           return span.getURL();
360         }
361       }
362       fulltext.append(text);
363     }
364     throw new ProvisioningException("Subscribe link not found: " + fulltext);
365   }
366 
createTransactionId()367   private String createTransactionId() {
368     return String.valueOf(Math.abs(new Random().nextLong()));
369   }
370 
extractText(String xml, String tag)371   private String extractText(String xml, String tag) throws ProvisioningException {
372     Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
373     Matcher matcher = pattern.matcher(xml);
374     if (matcher.find()) {
375       return matcher.group(1);
376     }
377     throw new ProvisioningException("Tag " + tag + " not found in xml response");
378   }
379 
380   private static class NetworkSpecifiedHurlStack extends HurlStack {
381 
382     private final Network network;
383 
NetworkSpecifiedHurlStack(Network network)384     public NetworkSpecifiedHurlStack(Network network) {
385       this.network = network;
386     }
387 
388     @Override
createConnection(URL url)389     protected HttpURLConnection createConnection(URL url) throws IOException {
390       return (HttpURLConnection) network.openConnection(url);
391     }
392   }
393 }
394