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.content.Context;
21import android.net.Network;
22import android.os.Build;
23import android.os.Build.VERSION_CODES;
24import android.os.Bundle;
25import android.support.annotation.NonNull;
26import android.support.annotation.VisibleForTesting;
27import android.support.annotation.WorkerThread;
28import android.telecom.PhoneAccountHandle;
29import android.telephony.TelephonyManager;
30import android.text.Html;
31import android.text.Spanned;
32import android.text.style.URLSpan;
33import android.util.ArrayMap;
34import com.android.dialer.common.ConfigProviderBindings;
35import com.android.voicemail.impl.ActivationTask;
36import com.android.voicemail.impl.Assert;
37import com.android.voicemail.impl.OmtpEvents;
38import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
39import com.android.voicemail.impl.VoicemailStatus;
40import com.android.voicemail.impl.VvmLog;
41import com.android.voicemail.impl.sync.VvmNetworkRequest;
42import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
43import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
44import com.android.volley.AuthFailureError;
45import com.android.volley.Request;
46import com.android.volley.RequestQueue;
47import com.android.volley.toolbox.HurlStack;
48import com.android.volley.toolbox.RequestFuture;
49import com.android.volley.toolbox.StringRequest;
50import com.android.volley.toolbox.Volley;
51import java.io.IOException;
52import java.net.CookieHandler;
53import java.net.CookieManager;
54import java.net.HttpURLConnection;
55import java.net.URL;
56import java.util.ArrayList;
57import java.util.List;
58import java.util.Locale;
59import java.util.Map;
60import java.util.Random;
61import java.util.concurrent.ExecutionException;
62import java.util.concurrent.TimeUnit;
63import java.util.concurrent.TimeoutException;
64import java.util.regex.Matcher;
65import java.util.regex.Pattern;
66import org.json.JSONArray;
67import 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)
85public 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 mTask;
135  private final PhoneAccountHandle mHandle;
136  private final OmtpVvmCarrierConfigHelper mHelper;
137  private final VoicemailStatus.Editor mStatus;
138  private final Bundle mData;
139
140  private final String mNumber;
141
142  private RequestQueue mRequestQueue;
143
144  @VisibleForTesting
145  static class ProvisioningException extends Exception {
146
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  public Vvm3Subscriber(
163      ActivationTask task,
164      PhoneAccountHandle handle,
165      OmtpVvmCarrierConfigHelper helper,
166      VoicemailStatus.Editor status,
167      Bundle data) {
168    Assert.isNotMainThread();
169    mTask = task;
170    mHandle = handle;
171    mHelper = helper;
172    mStatus = status;
173    mData = data;
174
175    // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
176    // is not included in the status SMS, thus no other way to get the current phone number.
177    mNumber =
178        mHelper
179            .getContext()
180            .getSystemService(TelephonyManager.class)
181            .createForPhoneAccountHandle(mHandle)
182            .getLine1Number();
183  }
184
185  @WorkerThread
186  public void subscribe() {
187    Assert.isNotMainThread();
188    // Cellular data is required to subscribe.
189    // processSubscription() is called after network is available.
190    VvmLog.i(TAG, "Subscribing");
191
192    try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
193      Network network = wrapper.get();
194      VvmLog.d(TAG, "provisioning: network available");
195      mRequestQueue =
196          Volley.newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
197      processSubscription();
198    } catch (RequestFailedException e) {
199      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
200      mTask.fail();
201    }
202  }
203
204  private void processSubscription() {
205    try {
206      String gatewayUrl = getSelfProvisioningGateway();
207      String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
208      String subscribeLink =
209          findSubscribeLink(getSubscribeLinkPatterns(mHelper.getContext()), selfProvisionResponse);
210      clickSubscribeLink(subscribeLink);
211    } catch (ProvisioningException e) {
212      VvmLog.e(TAG, e.toString());
213      mTask.fail();
214    }
215  }
216
217  /** Get the URL to perform self-provisioning from the voicemail management gateway. */
218  private String getSelfProvisioningGateway() throws ProvisioningException {
219    VvmLog.i(TAG, "retrieving SPG URL");
220    String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
221    return extractText(response, SPG_URL_TAG);
222  }
223
224  /**
225   * Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
226   * might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
227   * cookie from this response and cellular data is required to click the link.
228   */
229  private String getSelfProvisionResponse(String url) throws ProvisioningException {
230    VvmLog.i(TAG, "Retrieving self provisioning response");
231
232    RequestFuture<String> future = RequestFuture.newFuture();
233
234    StringRequest stringRequest =
235        new StringRequest(Request.Method.POST, url, future, future) {
236          @Override
237          protected Map<String, String> getParams() {
238            Map<String, String> params = new ArrayMap<>();
239            params.put(SPG_VZW_MDN_PARAM, mNumber);
240            params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
241            params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
242            params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
243            // Language to display the subscription page. The page is never shown to the user
244            // so just use English.
245            params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
246            return params;
247          }
248        };
249
250    mRequestQueue.add(stringRequest);
251    try {
252      return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
253    } catch (InterruptedException | ExecutionException | TimeoutException e) {
254      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
255      throw new ProvisioningException(e.toString());
256    }
257  }
258
259  private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
260    VvmLog.i(TAG, "Clicking subscribe link");
261    RequestFuture<String> future = RequestFuture.newFuture();
262
263    StringRequest stringRequest =
264        new StringRequest(Request.Method.POST, subscribeLink, future, future);
265    mRequestQueue.add(stringRequest);
266    try {
267      // A new STATUS SMS will be sent after this request.
268      future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
269    } catch (TimeoutException | ExecutionException | InterruptedException e) {
270      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
271      throw new ProvisioningException(e.toString());
272    }
273    // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
274    // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
275    // manually retry if it took too long.
276  }
277
278  private String vvm3XmlRequest(String operation) throws ProvisioningException {
279    VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
280    String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
281    if (voicemailManagementGateway == null) {
282      VvmLog.e(TAG, "voicemailManagementGateway url unknown");
283      return null;
284    }
285    String transactionId = createTransactionId();
286    String body =
287        String.format(
288            Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, mNumber, operation, Build.MODEL);
289
290    RequestFuture<String> future = RequestFuture.newFuture();
291    StringRequest stringRequest =
292        new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
293          @Override
294          public byte[] getBody() throws AuthFailureError {
295            return body.getBytes();
296          }
297        };
298    mRequestQueue.add(stringRequest);
299
300    try {
301      String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
302      if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
303        throw new ProvisioningException("transactionId mismatch");
304      }
305      return response;
306    } catch (InterruptedException | ExecutionException | TimeoutException e) {
307      mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
308      throw new ProvisioningException(e.toString());
309    }
310  }
311
312  @VisibleForTesting
313  static List<Pattern> getSubscribeLinkPatterns(Context context) {
314    String patternsJsonString =
315        ConfigProviderBindings.get(context)
316            .getString(
317                VVM3_SUBSCRIBE_LINK_PATTERNS_JSON_ARRAY, VVM3_SUBSCRIBE_LINK_DEFAULT_PATTERNS);
318    List<Pattern> patterns = new ArrayList<>();
319    try {
320      JSONArray patternsArray = new JSONArray(patternsJsonString);
321      for (int i = 0; i < patternsArray.length(); i++) {
322        patterns.add(Pattern.compile(patternsArray.getString(i)));
323      }
324    } catch (JSONException e) {
325      throw new IllegalArgumentException("Unable to parse patterns" + e);
326    }
327    return patterns;
328  }
329
330  @VisibleForTesting
331  static String findSubscribeLink(@NonNull List<Pattern> patterns, String response)
332      throws ProvisioningException {
333    if (patterns.isEmpty()) {
334      throw new IllegalArgumentException("empty patterns");
335    }
336    Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
337    URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
338    StringBuilder fulltext = new StringBuilder();
339
340    for (URLSpan span : spans) {
341      String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
342      for (Pattern pattern : patterns) {
343        if (pattern.matcher(text).matches()) {
344          return span.getURL();
345        }
346      }
347      fulltext.append(text);
348    }
349    throw new ProvisioningException("Subscribe link not found: " + fulltext);
350  }
351
352  private String createTransactionId() {
353    return String.valueOf(Math.abs(new Random().nextLong()));
354  }
355
356  private String extractText(String xml, String tag) throws ProvisioningException {
357    Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
358    Matcher matcher = pattern.matcher(xml);
359    if (matcher.find()) {
360      return matcher.group(1);
361    }
362    throw new ProvisioningException("Tag " + tag + " not found in xml response");
363  }
364
365  private static class NetworkSpecifiedHurlStack extends HurlStack {
366
367    private final Network mNetwork;
368
369    public NetworkSpecifiedHurlStack(Network network) {
370      mNetwork = network;
371    }
372
373    @Override
374    protected HttpURLConnection createConnection(URL url) throws IOException {
375      return (HttpURLConnection) mNetwork.openConnection(url);
376    }
377  }
378}
379