394 lines
15 KiB
Java
394 lines
15 KiB
Java
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License
|
|
*/
|
|
|
|
package com.android.voicemail.impl.protocol;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.content.Context;
|
|
import android.net.Network;
|
|
import android.os.Build;
|
|
import android.os.Build.VERSION_CODES;
|
|
import android.os.Bundle;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.support.annotation.WorkerThread;
|
|
import android.telecom.PhoneAccountHandle;
|
|
import android.telephony.TelephonyManager;
|
|
import android.text.Html;
|
|
import android.text.Spanned;
|
|
import android.text.style.URLSpan;
|
|
import android.util.ArrayMap;
|
|
import com.android.dialer.configprovider.ConfigProviderComponent;
|
|
import com.android.voicemail.impl.ActivationTask;
|
|
import com.android.voicemail.impl.Assert;
|
|
import com.android.voicemail.impl.OmtpEvents;
|
|
import com.android.voicemail.impl.OmtpVvmCarrierConfigHelper;
|
|
import com.android.voicemail.impl.VoicemailStatus;
|
|
import com.android.voicemail.impl.VvmLog;
|
|
import com.android.voicemail.impl.sync.VvmNetworkRequest;
|
|
import com.android.voicemail.impl.sync.VvmNetworkRequest.NetworkWrapper;
|
|
import com.android.voicemail.impl.sync.VvmNetworkRequest.RequestFailedException;
|
|
import com.android.volley.AuthFailureError;
|
|
import com.android.volley.Request;
|
|
import com.android.volley.RequestQueue;
|
|
import com.android.volley.toolbox.HurlStack;
|
|
import com.android.volley.toolbox.RequestFuture;
|
|
import com.android.volley.toolbox.StringRequest;
|
|
import com.android.volley.toolbox.Volley;
|
|
import java.io.IOException;
|
|
import java.net.CookieHandler;
|
|
import java.net.CookieManager;
|
|
import java.net.HttpURLConnection;
|
|
import java.net.URL;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
|
|
/**
|
|
* Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
|
|
* when the user is unprovisioned. This could happen when the user is on a legacy service, or
|
|
* switched over from devices that used other type of visual voicemail.
|
|
*
|
|
* <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
|
|
* the self provisioning gateway URL that we can modify voicemail services.
|
|
*
|
|
* <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
|
|
* with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
|
|
* subscription. This link should be clicked through cellular network, and have cookies enabled.
|
|
*
|
|
* <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
|
|
* ready user.
|
|
*/
|
|
@TargetApi(VERSION_CODES.O)
|
|
public class Vvm3Subscriber {
|
|
|
|
private static final String TAG = "Vvm3Subscriber";
|
|
|
|
private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
|
|
private static final String SPG_URL_TAG = "spgurl";
|
|
private static final String TRANSACTION_ID_TAG = "transactionid";
|
|
// language=XML
|
|
private static final String VMG_XML_REQUEST_FORMAT =
|
|
""
|
|
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
|
|
+ "<VMGVVMRequest>"
|
|
+ " <MessageHeader>"
|
|
+ " <transactionid>%1$s</transactionid>"
|
|
+ " </MessageHeader>"
|
|
+ " <MessageBody>"
|
|
+ " <mdn>%2$s</mdn>"
|
|
+ " <operation>%3$s</operation>"
|
|
+ " <source>Device</source>"
|
|
+ " <devicemodel>%4$s</devicemodel>"
|
|
+ " </MessageBody>"
|
|
+ "</VMGVVMRequest>";
|
|
|
|
static final String VMG_URL_KEY = "vmg_url";
|
|
|
|
// Self provisioning POST key/values. VVM3 API 2.1.0 12.3
|
|
private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
|
|
private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
|
|
private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
|
|
private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
|
|
// Value for all android device
|
|
private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
|
|
private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
|
|
private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
|
|
private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
|
|
private static final String SPG_LANGUAGE_EN = "ENGLISH";
|
|
|
|
@VisibleForTesting
|
|
static final String VVM3_SUBSCRIBE_LINK_PATTERNS_JSON_ARRAY =
|
|
"vvm3_subscribe_link_pattern_json_array";
|
|
|
|
private static final String VVM3_SUBSCRIBE_LINK_DEFAULT_PATTERNS =
|
|
"["
|
|
+ "\"(?i)Subscribe to Basic Visual Voice Mail\","
|
|
+ "\"(?i)Subscribe to Basic Visual Voicemail\""
|
|
+ "]";
|
|
|
|
private static final int REQUEST_TIMEOUT_SECONDS = 30;
|
|
|
|
private final ActivationTask task;
|
|
private final PhoneAccountHandle handle;
|
|
private final OmtpVvmCarrierConfigHelper helper;
|
|
private final VoicemailStatus.Editor status;
|
|
private final Bundle data;
|
|
|
|
private final String number;
|
|
|
|
private RequestQueue requestQueue;
|
|
|
|
@VisibleForTesting
|
|
static class ProvisioningException extends Exception {
|
|
|
|
public ProvisioningException(String message) {
|
|
super(message);
|
|
}
|
|
}
|
|
|
|
static {
|
|
// Set the default cookie handler to retain session data for the self provisioning gateway.
|
|
// Note; this is not ideal as it is application-wide, and can easily get clobbered.
|
|
// But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
|
|
// managing cookies will greatly increase complexity.
|
|
CookieManager cookieManager = new CookieManager();
|
|
CookieHandler.setDefault(cookieManager);
|
|
}
|
|
|
|
@WorkerThread
|
|
@SuppressWarnings("missingPermission")
|
|
public Vvm3Subscriber(
|
|
ActivationTask task,
|
|
PhoneAccountHandle handle,
|
|
OmtpVvmCarrierConfigHelper helper,
|
|
VoicemailStatus.Editor status,
|
|
Bundle data) {
|
|
Assert.isNotMainThread();
|
|
this.task = task;
|
|
this.handle = handle;
|
|
this.helper = helper;
|
|
this.status = status;
|
|
this.data = data;
|
|
|
|
// Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
|
|
// is not included in the status SMS, thus no other way to get the current phone number.
|
|
number =
|
|
stripInternational(
|
|
this.helper
|
|
.getContext()
|
|
.getSystemService(TelephonyManager.class)
|
|
.createForPhoneAccountHandle(this.handle)
|
|
.getLine1Number());
|
|
}
|
|
|
|
/**
|
|
* Self provisioning gateway expects 10 digit national format, but {@link
|
|
* TelephonyManager#getLine1Number()} might return e164 with "+1" at front.
|
|
*/
|
|
private static String stripInternational(String number) {
|
|
if (number.startsWith("+1")) {
|
|
number = number.substring(2);
|
|
}
|
|
return number;
|
|
}
|
|
|
|
@WorkerThread
|
|
public void subscribe() {
|
|
Assert.isNotMainThread();
|
|
// Cellular data is required to subscribe.
|
|
// processSubscription() is called after network is available.
|
|
VvmLog.i(TAG, "Subscribing");
|
|
|
|
try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(helper, handle, status)) {
|
|
Network network = wrapper.get();
|
|
VvmLog.d(TAG, "provisioning: network available");
|
|
requestQueue =
|
|
Volley.newRequestQueue(helper.getContext(), new NetworkSpecifiedHurlStack(network));
|
|
processSubscription();
|
|
} catch (RequestFailedException e) {
|
|
helper.handleEvent(status, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
|
|
task.fail();
|
|
}
|
|
}
|
|
|
|
private void processSubscription() {
|
|
try {
|
|
String gatewayUrl = getSelfProvisioningGateway();
|
|
String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
|
|
String subscribeLink =
|
|
findSubscribeLink(getSubscribeLinkPatterns(helper.getContext()), selfProvisionResponse);
|
|
clickSubscribeLink(subscribeLink);
|
|
} catch (ProvisioningException e) {
|
|
VvmLog.e(TAG, e.toString());
|
|
helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
|
|
task.fail();
|
|
}
|
|
}
|
|
|
|
/** Get the URL to perform self-provisioning from the voicemail management gateway. */
|
|
private String getSelfProvisioningGateway() throws ProvisioningException {
|
|
VvmLog.i(TAG, "retrieving SPG URL");
|
|
String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
|
|
return extractText(response, SPG_URL_TAG);
|
|
}
|
|
|
|
/**
|
|
* Sent a request to the self-provisioning gateway, which will return us with a webpage. The page
|
|
* might contain a "Subscribe to Basic Visual Voice Mail" link to complete the subscription. The
|
|
* cookie from this response and cellular data is required to click the link.
|
|
*/
|
|
private String getSelfProvisionResponse(String url) throws ProvisioningException {
|
|
VvmLog.i(TAG, "Retrieving self provisioning response");
|
|
|
|
RequestFuture<String> future = RequestFuture.newFuture();
|
|
|
|
StringRequest stringRequest =
|
|
new StringRequest(Request.Method.POST, url, future, future) {
|
|
@Override
|
|
protected Map<String, String> getParams() {
|
|
Map<String, String> params = new ArrayMap<>();
|
|
params.put(SPG_VZW_MDN_PARAM, number);
|
|
params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
|
|
params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
|
|
params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
|
|
// Language to display the subscription page. The page is never shown to the user
|
|
// so just use English.
|
|
params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
|
|
return params;
|
|
}
|
|
};
|
|
|
|
requestQueue.add(stringRequest);
|
|
try {
|
|
return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
|
helper.handleEvent(status, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
|
|
throw new ProvisioningException(e.toString());
|
|
}
|
|
}
|
|
|
|
private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
|
|
VvmLog.i(TAG, "Clicking subscribe link");
|
|
RequestFuture<String> future = RequestFuture.newFuture();
|
|
|
|
StringRequest stringRequest =
|
|
new StringRequest(Request.Method.POST, subscribeLink, future, future);
|
|
requestQueue.add(stringRequest);
|
|
try {
|
|
// A new STATUS SMS will be sent after this request.
|
|
future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
} catch (TimeoutException | ExecutionException | InterruptedException e) {
|
|
helper.handleEvent(status, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
|
|
throw new ProvisioningException(e.toString());
|
|
}
|
|
// It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
|
|
// Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
|
|
// manually retry if it took too long.
|
|
}
|
|
|
|
private String vvm3XmlRequest(String operation) throws ProvisioningException {
|
|
VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
|
|
String voicemailManagementGateway = data.getString(VMG_URL_KEY);
|
|
if (voicemailManagementGateway == null) {
|
|
VvmLog.e(TAG, "voicemailManagementGateway url unknown");
|
|
return null;
|
|
}
|
|
String transactionId = createTransactionId();
|
|
String body =
|
|
String.format(
|
|
Locale.US, VMG_XML_REQUEST_FORMAT, transactionId, number, operation, Build.MODEL);
|
|
|
|
RequestFuture<String> future = RequestFuture.newFuture();
|
|
StringRequest stringRequest =
|
|
new StringRequest(Request.Method.POST, voicemailManagementGateway, future, future) {
|
|
@Override
|
|
public byte[] getBody() throws AuthFailureError {
|
|
return body.getBytes();
|
|
}
|
|
};
|
|
requestQueue.add(stringRequest);
|
|
|
|
try {
|
|
String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
|
|
throw new ProvisioningException("transactionId mismatch");
|
|
}
|
|
return response;
|
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
|
helper.handleEvent(status, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
|
|
throw new ProvisioningException(e.toString());
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static List<Pattern> getSubscribeLinkPatterns(Context context) {
|
|
String patternsJsonString =
|
|
ConfigProviderComponent.get(context)
|
|
.getConfigProvider()
|
|
.getString(
|
|
VVM3_SUBSCRIBE_LINK_PATTERNS_JSON_ARRAY, VVM3_SUBSCRIBE_LINK_DEFAULT_PATTERNS);
|
|
List<Pattern> patterns = new ArrayList<>();
|
|
try {
|
|
JSONArray patternsArray = new JSONArray(patternsJsonString);
|
|
for (int i = 0; i < patternsArray.length(); i++) {
|
|
patterns.add(Pattern.compile(patternsArray.getString(i)));
|
|
}
|
|
} catch (JSONException e) {
|
|
throw new IllegalArgumentException("Unable to parse patterns" + e);
|
|
}
|
|
return patterns;
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static String findSubscribeLink(@NonNull List<Pattern> patterns, String response)
|
|
throws ProvisioningException {
|
|
if (patterns.isEmpty()) {
|
|
throw new IllegalArgumentException("empty patterns");
|
|
}
|
|
Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
|
|
URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
|
|
StringBuilder fulltext = new StringBuilder();
|
|
|
|
for (URLSpan span : spans) {
|
|
String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
|
|
for (Pattern pattern : patterns) {
|
|
if (pattern.matcher(text).matches()) {
|
|
return span.getURL();
|
|
}
|
|
}
|
|
fulltext.append(text);
|
|
}
|
|
throw new ProvisioningException("Subscribe link not found: " + fulltext);
|
|
}
|
|
|
|
private String createTransactionId() {
|
|
return String.valueOf(Math.abs(new Random().nextLong()));
|
|
}
|
|
|
|
private String extractText(String xml, String tag) throws ProvisioningException {
|
|
Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
|
|
Matcher matcher = pattern.matcher(xml);
|
|
if (matcher.find()) {
|
|
return matcher.group(1);
|
|
}
|
|
throw new ProvisioningException("Tag " + tag + " not found in xml response");
|
|
}
|
|
|
|
private static class NetworkSpecifiedHurlStack extends HurlStack {
|
|
|
|
private final Network network;
|
|
|
|
public NetworkSpecifiedHurlStack(Network network) {
|
|
this.network = network;
|
|
}
|
|
|
|
@Override
|
|
protected HttpURLConnection createConnection(URL url) throws IOException {
|
|
return (HttpURLConnection) network.openConnection(url);
|
|
}
|
|
}
|
|
}
|