From 6bb41a42cbabb6fba57e28db7425868d0e33e5a0 Mon Sep 17 00:00:00 2001 From: fionaxu Date: Fri, 10 Mar 2017 10:16:09 -0800 Subject: [PATCH] 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 --- .../CarrierDefaultApp/AndroidManifest.xml | 13 +- .../assets/quantum_ic_warning_amber_96.png | Bin 0 -> 682 bytes .../res/drawable/ic_sim_card.xml | 2 +- .../layout/activity_captive_portal_login.xml | 34 ++ .../CarrierDefaultApp/res/values/dimens.xml | 3 + .../CarrierDefaultApp/res/values/strings.xml | 9 +- .../CarrierDefaultApp/res/values/styles.xml | 15 +- .../CaptivePortalLaunchActivity.java | 233 ---------- .../CaptivePortalLoginActivity.java | 433 ++++++++++++++++++ .../carrierdefaultapp/CarrierActionUtils.java | 4 +- .../LaunchCaptivePortalActivityTest.java | 108 ----- 11 files changed, 501 insertions(+), 353 deletions(-) create mode 100644 packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png create mode 100644 packages/CarrierDefaultApp/res/layout/activity_captive_portal_login.xml delete mode 100644 packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java create mode 100644 packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java delete mode 100644 packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 2e642ec63cac8..8df194c118006 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -25,7 +25,6 @@ - @@ -34,10 +33,16 @@ - + + + + + diff --git a/packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png b/packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png new file mode 100644 index 0000000000000000000000000000000000000000..08294cee4587557b09659d656dff4433797e08da GIT binary patch literal 682 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U@G)aKnlxNuxcuwao}2$Qo}D`T@S|c$?TZycaE( z$nNZB+mPGY{Wn5NVVfA^M%FD-2bRgo8E$3XCiNiS^2@dPYd&T+2%Al_&jr##Tfc5~ zt&w_g_eE1~qZhMJc5iw_=`k3h%Y9D_fRyIGCsM{--@ADg?nIpbp!$5q z$7a3Hs*GuW7FgGuR7?~3^mCH)nHS!(1^)8iuj9Bd^P>M#@0zqP#y=-17~AQqAD#Z= z%!~9-s&>Y|X4Z6`@2OLZe`LIP{z+tE&;OlSpH*wqaz3l(roH*B`uF|Sx7FN#XQ%#t z<~LjNca0~4h@SDQ$$f%keY{_nex`Bqd?>L$^IHAnelz25KPRCPP)BT;x$*pT`~=XQ z)eR@7f0?=QEQI)RCga&>)nvEn`h_zKYdTN&Job)BlZk(1^!SI+bjjn>cbu`1- zc)tG6_8ar&`k(9nVf}FH{>^t8vYB%lwlbO>xWyoyklVn!;UB}78sUPpfX<7+G|1rT L>gTe~DWM4f@={Py literal 0 HcmV?d00001 diff --git a/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml index dc54fe2a3fac4..75aa40522a8ea 100644 --- a/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml +++ b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml @@ -22,4 +22,4 @@ - \ No newline at end of file + diff --git a/packages/CarrierDefaultApp/res/layout/activity_captive_portal_login.xml b/packages/CarrierDefaultApp/res/layout/activity_captive_portal_login.xml new file mode 100644 index 0000000000000..528576b57e5a5 --- /dev/null +++ b/packages/CarrierDefaultApp/res/layout/activity_captive_portal_login.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/packages/CarrierDefaultApp/res/values/dimens.xml b/packages/CarrierDefaultApp/res/values/dimens.xml index a3c5049bfd8b4..1ea8c351293dc 100644 --- a/packages/CarrierDefaultApp/res/values/dimens.xml +++ b/packages/CarrierDefaultApp/res/values/dimens.xml @@ -1,3 +1,6 @@ 32dp + + 16dp + 16dp diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml index fe5669d790778..1ea74bcab8db5 100644 --- a/packages/CarrierDefaultApp/res/values/strings.xml +++ b/packages/CarrierDefaultApp/res/values/strings.xml @@ -6,9 +6,8 @@ No Mobile data service Tap to add funds to your %s SIM Please contact your service provider %s - Connecting to captive portal... - Network timeout, would you like to retry? - Network unavailable - Quit - Wait + Sign in to mobile network + The network you’re trying to join has security issues. + For example, the login page may not belong to the organization shown. + Continue anyway via browser diff --git a/packages/CarrierDefaultApp/res/values/styles.xml b/packages/CarrierDefaultApp/res/values/styles.xml index 3d2691505f56e..939c1aa4c5dad 100644 --- a/packages/CarrierDefaultApp/res/values/styles.xml +++ b/packages/CarrierDefaultApp/res/values/styles.xml @@ -1,3 +1,16 @@ - + + + diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java deleted file mode 100644 index b7fde12f1d22f..0000000000000 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java +++ /dev/null @@ -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); - } -} diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java new file mode 100644 index 0000000000000..ec4c00ed61171 --- /dev/null +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLoginActivity.java @@ -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 = "


" + + "

%s
" + + "
%s
" + "%s"; + + @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); + } + +} diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java index d9bd2fcc0acb0..73ff3a9b5d1e6 100644 --- a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java +++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java @@ -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, diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java deleted file mode 100644 index 8a18d72294358..0000000000000 --- a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java +++ /dev/null @@ -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 { - - @Mock - private ConnectivityManager mCm; - @Mock - private NetworkInfo mNetworkInfo; - @Mock - private Network mNetwork; - - @Captor - private ArgumentCaptor mInt; - @Captor - private ArgumentCaptor 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(); - } -}