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