integrate portal webview to the default app

Based on the UX review feedback, we plan to implement portal webview
function inside the carrier default app instead of reusing the existing
portal app. This will give us more flexibility and control, also will
improve UX flow by getting rid of the some unwanted dialogues.

new added CaptivePortalLoginActivity is a copy paste from
com.android.captiveportallogin/CaptivePortalLoginActivity combined
with logic from deleted LaunchCaptivePortalActivity.
All webview UI was inherited from com.android.captiveportal

Test: Manual
Bug: 36002256
Merged-in: I2627d5a43039ce433006c058bb4f2c1a39113e59
Change-Id: If422fa12c5f24d9b9e2c9380b3edf94df74bb85f
This commit is contained in:
fionaxu
2017-03-10 10:16:09 -08:00
parent 3dee29581e
commit 6bb41a42cb
11 changed files with 501 additions and 353 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -6,9 +6,8 @@
<string name="no_data_notification_id">No Mobile data service</string>
<string name="portal_notification_detail">Tap to add funds to your %s SIM</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&#8217;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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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();
}
}