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