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:
@@ -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">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’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