Merge "integrate portal webview to the default app"
This commit is contained in:
@@ -25,7 +25,6 @@
|
||||
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME" />
|
||||
|
||||
<application android:label="@string/app_name" >
|
||||
@@ -34,10 +33,16 @@
|
||||
<action android:name="com.android.internal.telephony.CARRIER_SIGNAL_REDIRECTED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<activity android:name="com.android.carrierdefaultapp.CaptivePortalLaunchActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:excludeFromRecents="true"/>
|
||||
<service android:name="com.android.carrierdefaultapp.ProvisionObserver"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||
<activity
|
||||
android:name="com.android.carrierdefaultapp.CaptivePortalLoginActivity"
|
||||
android:label="@string/action_bar_label"
|
||||
android:theme="@style/AppTheme"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize" >
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 682 B |
@@ -22,4 +22,4 @@
|
||||
<path
|
||||
android:fillColor="#757575"
|
||||
android:pathData="M18,2h-8L4.02,8 4,20c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM13,17h-2v-2h2v2zM13,13h-2L11,8h2v5z"/>
|
||||
</vector>
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.android.carrierdefaultapp.CaptivePortalLoginActivity"
|
||||
tools:ignore="MergeRootFrame">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/url_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20sp"
|
||||
android:singleLine="true" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="?android:attr/progressBarStyleHorizontal" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentBottom="false"
|
||||
android:layout_alignParentRight="false" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -1,3 +1,6 @@
|
||||
<resources>
|
||||
<dimen name="glif_icon_size">32dp</dimen>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
</resources>
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
<string name="no_data_notification_id">Your mobile data has been deactivated</string>
|
||||
<string name="portal_notification_detail">Tap to visit the %s website</string>
|
||||
<string name="no_data_notification_detail">Please contact your service provider %s</string>
|
||||
<string name="progress_dialogue_network_connection">Connecting to captive portal...</string>
|
||||
<string name="alert_dialogue_network_timeout">Network timeout, would you like to retry?</string>
|
||||
<string name="alert_dialogue_network_timeout_title">Network unavailable</string>
|
||||
<string name="quit">Quit</string>
|
||||
<string name="wait">Wait</string>
|
||||
<string name="action_bar_label">Sign in to mobile network</string>
|
||||
<string name="ssl_error_warning">The network you’re trying to join has security issues.</string>
|
||||
<string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string>
|
||||
<string name="ssl_error_continue">Continue anyway via browser</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<resources>
|
||||
<style name="AlertDialog" parent="android:Theme.Material.Light.Dialog.Alert"/>
|
||||
<style name="AppBaseTheme" parent="@android:style/Theme.Material.Settings">
|
||||
<!--
|
||||
Theme customizations available in newer API levels can go in
|
||||
res/values-vXX/styles.xml, while customizations related to
|
||||
backward-compatibility can go here.
|
||||
-->
|
||||
</style>
|
||||
|
||||
<!-- Application theme. -->
|
||||
<style name="AppTheme" parent="AppBaseTheme">
|
||||
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
|
||||
<!-- Setting's theme's accent color makes ProgressBar useless, reset back. -->
|
||||
<item name="android:colorAccent">@*android:color/material_deep_teal_500</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1,233 +0,0 @@
|
||||
/*
|
||||
* 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.carrierdefaultapp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.CaptivePortal;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.CarrierConfigManager;
|
||||
import android.telephony.Rlog;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.text.TextUtils;
|
||||
import android.net.ICaptivePortal;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.WindowManager;
|
||||
import com.android.carrierdefaultapp.R;
|
||||
import com.android.internal.telephony.PhoneConstants;
|
||||
import com.android.internal.telephony.TelephonyIntents;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
|
||||
import static android.net.CaptivePortal.APP_RETURN_DISMISSED;
|
||||
|
||||
/**
|
||||
* Activity that launches in response to the captive portal notification
|
||||
* @see com.android.carrierdefaultapp.CarrierActionUtils#CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION
|
||||
* This activity requests network connection if there is no available one, launches the
|
||||
* {@link com.android.captiveportallogin portalApp} and keeps track of the portal activation result.
|
||||
*/
|
||||
public class CaptivePortalLaunchActivity extends Activity {
|
||||
private static final String TAG = CaptivePortalLaunchActivity.class.getSimpleName();
|
||||
private static final boolean DBG = true;
|
||||
public static final int NETWORK_REQUEST_TIMEOUT_IN_MS = 5 * 1000;
|
||||
|
||||
private ConnectivityManager mCm = null;
|
||||
private ConnectivityManager.NetworkCallback mCb = null;
|
||||
/* Progress dialogue when request network connection for captive portal */
|
||||
private AlertDialog mProgressDialog = null;
|
||||
/* Alert dialogue when network request is timeout */
|
||||
private AlertDialog mAlertDialog = null;
|
||||
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mCm = ConnectivityManager.from(this);
|
||||
// Check network connection before loading portal
|
||||
Network network = getNetworkForCaptivePortal();
|
||||
NetworkInfo nwInfo = mCm.getNetworkInfo(network);
|
||||
if (nwInfo == null || !nwInfo.isConnected()) {
|
||||
if (DBG) logd("Network unavailable, request restricted connection");
|
||||
requestNetwork(getIntent());
|
||||
} else {
|
||||
launchCaptivePortal(getIntent(), network);
|
||||
}
|
||||
}
|
||||
|
||||
// show progress dialog during network connecting
|
||||
private void showConnectingProgressDialog() {
|
||||
mProgressDialog = new ProgressDialog(getApplicationContext());
|
||||
mProgressDialog.setMessage(getString(R.string.progress_dialogue_network_connection));
|
||||
mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
|
||||
mProgressDialog.show();
|
||||
}
|
||||
|
||||
// if network request is timeout, show alert dialog with two option: cancel & wait
|
||||
private void showConnectionTimeoutAlertDialog() {
|
||||
mAlertDialog = new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AlertDialog))
|
||||
.setMessage(getString(R.string.alert_dialogue_network_timeout))
|
||||
.setTitle(getString(R.string.alert_dialogue_network_timeout_title))
|
||||
.setNegativeButton(getString(R.string.quit),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// cancel
|
||||
dismissDialog(mAlertDialog);
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setPositiveButton(getString(R.string.wait),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
// wait, request network again
|
||||
dismissDialog(mAlertDialog);
|
||||
requestNetwork(getIntent());
|
||||
}
|
||||
})
|
||||
.create();
|
||||
mAlertDialog.show();
|
||||
}
|
||||
|
||||
private void requestNetwork(final Intent intent) {
|
||||
NetworkRequest request = new NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build();
|
||||
|
||||
mCb = new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
if (DBG) logd("Network available: " + network);
|
||||
dismissDialog(mProgressDialog);
|
||||
mCm.bindProcessToNetwork(network);
|
||||
launchCaptivePortal(intent, network);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnavailable() {
|
||||
if (DBG) logd("Network unavailable");
|
||||
dismissDialog(mProgressDialog);
|
||||
showConnectionTimeoutAlertDialog();
|
||||
}
|
||||
};
|
||||
showConnectingProgressDialog();
|
||||
mCm.requestNetwork(request, mCb, NETWORK_REQUEST_TIMEOUT_IN_MS);
|
||||
}
|
||||
|
||||
private void releaseNetworkRequest() {
|
||||
logd("release Network Request");
|
||||
if (mCb != null) {
|
||||
mCm.unregisterNetworkCallback(mCb);
|
||||
mCb = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissDialog(AlertDialog dialog) {
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private Network getNetworkForCaptivePortal() {
|
||||
Network[] info = mCm.getAllNetworks();
|
||||
if (!ArrayUtils.isEmpty(info)) {
|
||||
for (Network nw : info) {
|
||||
final NetworkCapabilities nc = mCm.getNetworkCapabilities(nw);
|
||||
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
&& nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
||||
return nw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void launchCaptivePortal(final Intent intent, Network network) {
|
||||
String redirectUrl = intent.getStringExtra(TelephonyIntents.EXTRA_REDIRECTION_URL_KEY);
|
||||
int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
|
||||
SubscriptionManager.getDefaultVoiceSubscriptionId());
|
||||
if (TextUtils.isEmpty(redirectUrl) || !matchUrl(redirectUrl, subId)) {
|
||||
loge("Launch portal fails due to incorrect redirection URL: " +
|
||||
Rlog.pii(TAG, redirectUrl));
|
||||
return;
|
||||
}
|
||||
final Intent portalIntent = new Intent(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
|
||||
portalIntent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
|
||||
portalIntent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
|
||||
new CaptivePortal(new ICaptivePortal.Stub() {
|
||||
@Override
|
||||
public void appResponse(int response) {
|
||||
logd("portal response code: " + response);
|
||||
releaseNetworkRequest();
|
||||
if (response == APP_RETURN_DISMISSED) {
|
||||
// Upon success http response code, trigger re-evaluation
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_ENABLE_RADIO, intent,
|
||||
getApplicationContext());
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_ENABLE_METERED_APNS, intent,
|
||||
getApplicationContext());
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_CANCEL_ALL_NOTIFICATIONS,
|
||||
intent, getApplicationContext());
|
||||
}
|
||||
}
|
||||
}));
|
||||
portalIntent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, redirectUrl);
|
||||
portalIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
if (DBG) logd("launching portal");
|
||||
startActivity(portalIntent);
|
||||
finish();
|
||||
}
|
||||
|
||||
// match configured redirection url
|
||||
private boolean matchUrl(String url, int subId) {
|
||||
CarrierConfigManager configManager = getApplicationContext()
|
||||
.getSystemService(CarrierConfigManager.class);
|
||||
String[] redirectURLs = configManager.getConfigForSubId(subId).getStringArray(
|
||||
CarrierConfigManager.KEY_CARRIER_DEFAULT_REDIRECTION_URL_STRING_ARRAY);
|
||||
if (ArrayUtils.isEmpty(redirectURLs)) {
|
||||
if (DBG) logd("match is unnecessary without any configured redirection url");
|
||||
return true;
|
||||
}
|
||||
for (String redirectURL : redirectURLs) {
|
||||
if (url.startsWith(redirectURL)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (DBG) loge("no match found for configured redirection url");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void logd(String s) {
|
||||
Rlog.d(TAG, s);
|
||||
}
|
||||
|
||||
private static void loge(String s) {
|
||||
Rlog.d(TAG, s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.carrierdefaultapp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.LoadedApk;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.ConnectivityManager.NetworkCallback;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.Proxy;
|
||||
import android.net.TrafficStats;
|
||||
import android.net.Uri;
|
||||
import android.net.http.SslError;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.CarrierConfigManager;
|
||||
import android.telephony.Rlog;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.webkit.SslErrorHandler;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.telephony.PhoneConstants;
|
||||
import com.android.internal.telephony.TelephonyIntents;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Activity that launches in response to the captive portal notification
|
||||
* @see com.android.carrierdefaultapp.CarrierActionUtils#CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION
|
||||
* This activity requests network connection if there is no available one before loading the real
|
||||
* portal page and apply carrier actions on the portal activation result.
|
||||
*/
|
||||
public class CaptivePortalLoginActivity extends Activity {
|
||||
private static final String TAG = CaptivePortalLoginActivity.class.getSimpleName();
|
||||
private static final boolean DBG = true;
|
||||
|
||||
private static final int SOCKET_TIMEOUT_MS = 10 * 1000;
|
||||
private static final int NETWORK_REQUEST_TIMEOUT_MS = 5 * 1000;
|
||||
|
||||
private URL mUrl;
|
||||
private Network mNetwork;
|
||||
private NetworkCallback mNetworkCallback;
|
||||
private ConnectivityManager mCm;
|
||||
private WebView mWebView;
|
||||
private MyWebViewClient mWebViewClient;
|
||||
private boolean mLaunchBrowser = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mCm = ConnectivityManager.from(this);
|
||||
mUrl = getUrlForCaptivePortal();
|
||||
if (mUrl == null) {
|
||||
done(false);
|
||||
return;
|
||||
}
|
||||
if (DBG) logd(String.format("onCreate for %s", mUrl.toString()));
|
||||
setContentView(R.layout.activity_captive_portal_login);
|
||||
getActionBar().setDisplayShowHomeEnabled(false);
|
||||
|
||||
mWebView = (WebView) findViewById(R.id.webview);
|
||||
mWebView.clearCache(true);
|
||||
WebSettings webSettings = mWebView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
|
||||
mWebViewClient = new MyWebViewClient();
|
||||
mWebView.setWebViewClient(mWebViewClient);
|
||||
mWebView.setWebChromeClient(new MyWebChromeClient());
|
||||
|
||||
mNetwork = getNetworkForCaptivePortal();
|
||||
if (mNetwork == null) {
|
||||
requestNetworkForCaptivePortal();
|
||||
} else {
|
||||
mCm.bindProcessToNetwork(mNetwork);
|
||||
// Start initial page load so WebView finishes loading proxy settings.
|
||||
// Actual load of mUrl is initiated by MyWebViewClient.
|
||||
mWebView.loadData("", "text/html", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
WebView myWebView = (WebView) findViewById(R.id.webview);
|
||||
if (myWebView.canGoBack() && mWebViewClient.allowBack()) {
|
||||
myWebView.goBack();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
releaseNetworkRequest();
|
||||
if (mLaunchBrowser) {
|
||||
// Give time for this network to become default. After 500ms just proceed.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// TODO: This misses when mNetwork underlies a VPN.
|
||||
if (mNetwork.equals(mCm.getActiveNetwork())) break;
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
final String url = mUrl.toString();
|
||||
if (DBG) logd("starting activity with intent ACTION_VIEW for " + url);
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||
}
|
||||
}
|
||||
|
||||
// Find WebView's proxy BroadcastReceiver and prompt it to read proxy system properties.
|
||||
private void setWebViewProxy() {
|
||||
LoadedApk loadedApk = getApplication().mLoadedApk;
|
||||
try {
|
||||
Field receiversField = LoadedApk.class.getDeclaredField("mReceivers");
|
||||
receiversField.setAccessible(true);
|
||||
ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
|
||||
for (Object receiverMap : receivers.values()) {
|
||||
for (Object rec : ((ArrayMap) receiverMap).keySet()) {
|
||||
Class clazz = rec.getClass();
|
||||
if (clazz.getName().contains("ProxyChangeListener")) {
|
||||
Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class,
|
||||
Intent.class);
|
||||
Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
|
||||
onReceiveMethod.invoke(rec, getApplicationContext(), intent);
|
||||
Log.v(TAG, "Prompting WebView proxy reload.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
loge("Exception while setting WebView proxy: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private void done(boolean success) {
|
||||
if (DBG) logd(String.format("Result success %b for %s", success, mUrl.toString()));
|
||||
if (success) {
|
||||
// Trigger re-evaluation upon success http response code
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_ENABLE_RADIO, getIntent(),
|
||||
getApplicationContext());
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_ENABLE_METERED_APNS, getIntent(),
|
||||
getApplicationContext());
|
||||
CarrierActionUtils.applyCarrierAction(
|
||||
CarrierActionUtils.CARRIER_ACTION_CANCEL_ALL_NOTIFICATIONS, getIntent(),
|
||||
getApplicationContext());
|
||||
|
||||
}
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
private URL getUrlForCaptivePortal() {
|
||||
String url = getIntent().getStringExtra(TelephonyIntents.EXTRA_REDIRECTION_URL_KEY);
|
||||
if (url.isEmpty()) {
|
||||
url = mCm.getCaptivePortalServerUrl();
|
||||
}
|
||||
final CarrierConfigManager configManager = getApplicationContext()
|
||||
.getSystemService(CarrierConfigManager.class);
|
||||
final int subId = getIntent().getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
|
||||
SubscriptionManager.getDefaultVoiceSubscriptionId());
|
||||
final String[] portalURLs = configManager.getConfigForSubId(subId).getStringArray(
|
||||
CarrierConfigManager.KEY_CARRIER_DEFAULT_REDIRECTION_URL_STRING_ARRAY);
|
||||
if (!ArrayUtils.isEmpty(portalURLs)) {
|
||||
for (String portalUrl : portalURLs) {
|
||||
if (url.startsWith(portalUrl)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
url = null;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (MalformedURLException e) {
|
||||
loge("Invalid captive portal URL " + url);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void testForCaptivePortal() {
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
// Give time for captive portal to open.
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
HttpURLConnection urlConnection = null;
|
||||
int httpResponseCode = 500;
|
||||
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_PROBE);
|
||||
try {
|
||||
urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl);
|
||||
urlConnection.setInstanceFollowRedirects(false);
|
||||
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
|
||||
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
|
||||
urlConnection.setUseCaches(false);
|
||||
urlConnection.getInputStream();
|
||||
httpResponseCode = urlConnection.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
} finally {
|
||||
if (urlConnection != null) urlConnection.disconnect();
|
||||
}
|
||||
if (httpResponseCode == 204) {
|
||||
done(true);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private Network getNetworkForCaptivePortal() {
|
||||
Network[] info = mCm.getAllNetworks();
|
||||
if (!ArrayUtils.isEmpty(info)) {
|
||||
for (Network nw : info) {
|
||||
final NetworkCapabilities nc = mCm.getNetworkCapabilities(nw);
|
||||
if (nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
&& nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
|
||||
return nw;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void requestNetworkForCaptivePortal() {
|
||||
NetworkRequest request = new NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||
.build();
|
||||
|
||||
mNetworkCallback = new ConnectivityManager.NetworkCallback() {
|
||||
@Override
|
||||
public void onAvailable(Network network) {
|
||||
if (DBG) logd("Network available: " + network);
|
||||
mCm.bindProcessToNetwork(network);
|
||||
mNetwork = network;
|
||||
runOnUiThreadIfNotFinishing(() -> {
|
||||
// Start initial page load so WebView finishes loading proxy settings.
|
||||
// Actual load of mUrl is initiated by MyWebViewClient.
|
||||
mWebView.loadData("", "text/html", null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnavailable() {
|
||||
if (DBG) logd("Network unavailable");
|
||||
runOnUiThreadIfNotFinishing(() -> {
|
||||
// Instead of not loading anything in webview, simply load the page and return
|
||||
// HTTP error page in the absence of network connection.
|
||||
mWebView.loadUrl(mUrl.toString());
|
||||
});
|
||||
}
|
||||
};
|
||||
logd("request Network for captive portal");
|
||||
mCm.requestNetwork(request, mNetworkCallback, NETWORK_REQUEST_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
private void releaseNetworkRequest() {
|
||||
logd("release Network for captive portal");
|
||||
if (mNetworkCallback != null) {
|
||||
mCm.unregisterNetworkCallback(mNetworkCallback);
|
||||
mNetworkCallback = null;
|
||||
mNetwork = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class MyWebViewClient extends WebViewClient {
|
||||
private static final String INTERNAL_ASSETS = "file:///android_asset/";
|
||||
private final String mBrowserBailOutToken = Long.toString(new Random().nextLong());
|
||||
// How many Android device-independent-pixels per scaled-pixel
|
||||
// dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
|
||||
private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
|
||||
getResources().getDisplayMetrics())
|
||||
/ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
|
||||
getResources().getDisplayMetrics());
|
||||
private int mPagesLoaded;
|
||||
|
||||
// If we haven't finished cleaning up the history, don't allow going back.
|
||||
public boolean allowBack() {
|
||||
return mPagesLoaded > 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
if (url.contains(mBrowserBailOutToken)) {
|
||||
mLaunchBrowser = true;
|
||||
done(false);
|
||||
return;
|
||||
}
|
||||
// The first page load is used only to cause the WebView to
|
||||
// fetch the proxy settings. Don't update the URL bar, and
|
||||
// don't check if the captive portal is still there.
|
||||
if (mPagesLoaded == 0) return;
|
||||
// For internally generated pages, leave URL bar listing prior URL as this is the URL
|
||||
// the page refers to.
|
||||
if (!url.startsWith(INTERNAL_ASSETS)) {
|
||||
final TextView myUrlBar = (TextView) findViewById(R.id.url_bar);
|
||||
myUrlBar.setText(url);
|
||||
}
|
||||
if (mNetwork != null) {
|
||||
testForCaptivePortal();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
mPagesLoaded++;
|
||||
if (mPagesLoaded == 1) {
|
||||
// Now that WebView has loaded at least one page we know it has read in the proxy
|
||||
// settings. Now prompt the WebView read the Network-specific proxy settings.
|
||||
setWebViewProxy();
|
||||
// Load the real page.
|
||||
view.loadUrl(mUrl.toString());
|
||||
return;
|
||||
} else if (mPagesLoaded == 2) {
|
||||
// Prevent going back to empty first page.
|
||||
view.clearHistory();
|
||||
}
|
||||
if (mNetwork != null) {
|
||||
testForCaptivePortal();
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Android device-independent-pixels (dp) to HTML size.
|
||||
private String dp(int dp) {
|
||||
// HTML px's are scaled just like dp's, so just add "px" suffix.
|
||||
return Integer.toString(dp) + "px";
|
||||
}
|
||||
|
||||
// Convert Android scaled-pixels (sp) to HTML size.
|
||||
private String sp(int sp) {
|
||||
// Convert sp to dp's.
|
||||
float dp = sp * mDpPerSp;
|
||||
// Apply a scale factor to make things look right.
|
||||
dp *= 1.3;
|
||||
// Convert dp's to HTML size.
|
||||
return dp((int) dp);
|
||||
}
|
||||
|
||||
// A web page consisting of a large broken lock icon to indicate SSL failure.
|
||||
private final String SSL_ERROR_HTML = "<html><head><style>"
|
||||
+ "body { margin-left:" + dp(48) + "; margin-right:" + dp(48) + "; "
|
||||
+ "margin-top:" + dp(96) + "; background-color:#fafafa; }"
|
||||
+ "img { width:" + dp(48) + "; height:" + dp(48) + "; }"
|
||||
+ "div.warn { font-size:" + sp(16) + "; margin-top:" + dp(16) + "; "
|
||||
+ " opacity:0.87; line-height:1.28; }"
|
||||
+ "div.example { font-size:" + sp(14) + "; margin-top:" + dp(16) + "; "
|
||||
+ " opacity:0.54; line-height:1.21905; }"
|
||||
+ "a { font-size:" + sp(14) + "; text-decoration:none; text-transform:uppercase; "
|
||||
+ " margin-top:" + dp(24) + "; display:inline-block; color:#4285F4; "
|
||||
+ " height:" + dp(48) + "; font-weight:bold; }"
|
||||
+ "</style></head><body><p><img src=quantum_ic_warning_amber_96.png><br>"
|
||||
+ "<div class=warn>%s</div>"
|
||||
+ "<div class=example>%s</div>" + "<a href=%s>%s</a></body></html>";
|
||||
|
||||
@Override
|
||||
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
|
||||
Log.w(TAG, "SSL error (error: " + error.getPrimaryError() + " host: "
|
||||
// Only show host to avoid leaking private info.
|
||||
+ Uri.parse(error.getUrl()).getHost() + " certificate: "
|
||||
+ error.getCertificate() + "); displaying SSL warning.");
|
||||
final String html = String.format(SSL_ERROR_HTML, getString(R.string.ssl_error_warning),
|
||||
getString(R.string.ssl_error_example), mBrowserBailOutToken,
|
||||
getString(R.string.ssl_error_continue));
|
||||
view.loadDataWithBaseURL(INTERNAL_ASSETS, html, "text/HTML", "UTF-8", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.startsWith("tel:")) {
|
||||
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(url)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class MyWebChromeClient extends WebChromeClient {
|
||||
@Override
|
||||
public void onProgressChanged(WebView view, int newProgress) {
|
||||
final ProgressBar myProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
|
||||
myProgressBar.setProgress(newProgress);
|
||||
}
|
||||
}
|
||||
|
||||
private void runOnUiThreadIfNotFinishing(Runnable r) {
|
||||
if (!isFinishing()) {
|
||||
runOnUiThread(r);
|
||||
}
|
||||
}
|
||||
|
||||
private static void logd(String s) {
|
||||
Rlog.d(TAG, s);
|
||||
}
|
||||
|
||||
private static void loge(String s) {
|
||||
Rlog.d(TAG, s);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -112,8 +112,10 @@ public class CarrierActionUtils {
|
||||
logd("onShowCaptivePortalNotification");
|
||||
final NotificationManager notificationMgr = context.getSystemService(
|
||||
NotificationManager.class);
|
||||
Intent portalIntent = new Intent(context, CaptivePortalLaunchActivity.class);
|
||||
Intent portalIntent = new Intent(context, CaptivePortalLoginActivity.class);
|
||||
portalIntent.putExtras(intent);
|
||||
portalIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
|
||||
| Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, portalIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
Notification notification = getNotification(context, R.string.portal_notification_id,
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package com.android.carrierdefaultapp;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.NetworkRequest;
|
||||
|
||||
import com.android.internal.telephony.TelephonyIntents;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class LaunchCaptivePortalActivityTest extends
|
||||
CarrierDefaultActivityTestCase<CaptivePortalLaunchActivity> {
|
||||
|
||||
@Mock
|
||||
private ConnectivityManager mCm;
|
||||
@Mock
|
||||
private NetworkInfo mNetworkInfo;
|
||||
@Mock
|
||||
private Network mNetwork;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<Integer> mInt;
|
||||
@Captor
|
||||
private ArgumentCaptor<NetworkRequest> mNetworkReq;
|
||||
|
||||
private NetworkCapabilities mNetworkCapabilities;
|
||||
|
||||
public LaunchCaptivePortalActivityTest() {
|
||||
super(CaptivePortalLaunchActivity.class);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
injectSystemService(ConnectivityManager.class, mCm);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Intent createActivityIntent() {
|
||||
Intent intent = new Intent(getInstrumentation().getTargetContext(),
|
||||
CaptivePortalLaunchActivity.class);
|
||||
intent.putExtra(TelephonyIntents.EXTRA_REDIRECTION_URL_KEY, "url");
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithoutInternetConnection() throws Throwable {
|
||||
startActivity();
|
||||
TestContext.waitForMs(100);
|
||||
verify(mCm, atLeast(1)).requestNetwork(mNetworkReq.capture(), any(), mInt.capture());
|
||||
// verify network request
|
||||
assert(mNetworkReq.getValue().networkCapabilities.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_INTERNET));
|
||||
assert(mNetworkReq.getValue().networkCapabilities.hasTransport(
|
||||
NetworkCapabilities.TRANSPORT_CELLULAR));
|
||||
assertFalse(mNetworkReq.getValue().networkCapabilities.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED));
|
||||
assertEquals(CaptivePortalLaunchActivity.NETWORK_REQUEST_TIMEOUT_IN_MS,
|
||||
(int) mInt.getValue());
|
||||
// verify captive portal app is not launched due to unavailable network
|
||||
assertNull(getStartedActivityIntent());
|
||||
stopActivity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithInternetConnection() throws Throwable {
|
||||
// Mock internet connection
|
||||
mNetworkCapabilities = new NetworkCapabilities()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
|
||||
doReturn(new Network[]{mNetwork}).when(mCm).getAllNetworks();
|
||||
doReturn(mNetworkCapabilities).when(mCm).getNetworkCapabilities(eq(mNetwork));
|
||||
doReturn(mNetworkInfo).when(mCm).getNetworkInfo(eq(mNetwork));
|
||||
doReturn(true).when(mNetworkInfo).isConnected();
|
||||
|
||||
startActivity();
|
||||
TestContext.waitForMs(100);
|
||||
// verify there is no network request with internet connection
|
||||
verify(mCm, times(0)).requestNetwork(any(), any(), anyInt());
|
||||
// verify captive portal app is launched
|
||||
assertNotNull(getStartedActivityIntent());
|
||||
assertEquals(ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN,
|
||||
getStartedActivityIntent().getAction());
|
||||
stopActivity();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user