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