Home | History | Annotate | Download | only in protocol
      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.ConfigProviderBindings;
     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 
    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     this.task = task;
    170     this.handle = handle;
    171     this.helper = helper;
    172     this.status = status;
    173     this.data = 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     number =
    178         this.helper
    179             .getContext()
    180             .getSystemService(TelephonyManager.class)
    181             .createForPhoneAccountHandle(this.handle)
    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(helper, handle, status)) {
    193       Network network = wrapper.get();
    194       VvmLog.d(TAG, "provisioning: network available");
    195       requestQueue =
    196           Volley.newRequestQueue(helper.getContext(), new NetworkSpecifiedHurlStack(network));
    197       processSubscription();
    198     } catch (RequestFailedException e) {
    199       helper.handleEvent(status, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
    200       task.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(helper.getContext()), selfProvisionResponse);
    210       clickSubscribeLink(subscribeLink);
    211     } catch (ProvisioningException e) {
    212       VvmLog.e(TAG, e.toString());
    213       task.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, number);
    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     requestQueue.add(stringRequest);
    251     try {
    252       return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
    253     } catch (InterruptedException | ExecutionException | TimeoutException e) {
    254       helper.handleEvent(status, 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     requestQueue.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       helper.handleEvent(status, 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 = data.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, number, 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     requestQueue.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       helper.handleEvent(status, 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 network;
    368 
    369     public NetworkSpecifiedHurlStack(Network network) {
    370       this.network = network;
    371     }
    372 
    373     @Override
    374     protected HttpURLConnection createConnection(URL url) throws IOException {
    375       return (HttpURLConnection) network.openConnection(url);
    376     }
    377   }
    378 }
    379