diff options
11 files changed, 501 insertions, 353 deletions
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml index 2e642ec63cac..8df194c11800 100644 --- a/packages/CarrierDefaultApp/AndroidManifest.xml +++ b/packages/CarrierDefaultApp/AndroidManifest.xml @@ -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> diff --git a/packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png b/packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png Binary files differnew file mode 100644 index 000000000000..08294cee4587 --- /dev/null +++ b/packages/CarrierDefaultApp/assets/quantum_ic_warning_amber_96.png diff --git a/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml index dc54fe2a3fac..75aa40522a8e 100644 --- a/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml +++ b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml @@ -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>
\ No newline at end of file +</vector> 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 000000000000..528576b57e5a --- /dev/null +++ b/packages/CarrierDefaultApp/res/layout/activity_captive_portal_login.xml @@ -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> diff --git a/packages/CarrierDefaultApp/res/values/dimens.xml b/packages/CarrierDefaultApp/res/values/dimens.xml index a3c5049bfd8b..1ea8c351293d 100644 --- a/packages/CarrierDefaultApp/res/values/dimens.xml +++ b/packages/CarrierDefaultApp/res/values/dimens.xml @@ -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> diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml index f904600df772..f9342b7c5899 100644 --- a/packages/CarrierDefaultApp/res/values/strings.xml +++ b/packages/CarrierDefaultApp/res/values/strings.xml @@ -6,9 +6,8 @@ <string name="no_data_notification_id">Your mobile data has been deactivated</string> <string name="portal_notification_detail">Tap to visit the %s website</string> <string name="no_data_notification_detail">Please contact your service provider %s</string> - <string name="progress_dialogue_network_connection">Connecting to captive portal...</string> - <string name="alert_dialogue_network_timeout">Network timeout, would you like to retry?</string> - <string name="alert_dialogue_network_timeout_title">Network unavailable</string> - <string name="quit">Quit</string> - <string name="wait">Wait</string> + <string name="action_bar_label">Sign in to mobile network</string> + <string name="ssl_error_warning">The network you’re trying to join has security issues.</string> + <string name="ssl_error_example">For example, the login page may not belong to the organization shown.</string> + <string name="ssl_error_continue">Continue anyway via browser</string> </resources> diff --git a/packages/CarrierDefaultApp/res/values/styles.xml b/packages/CarrierDefaultApp/res/values/styles.xml index 3d2691505f56..939c1aa4c5da 100644 --- a/packages/CarrierDefaultApp/res/values/styles.xml +++ b/packages/CarrierDefaultApp/res/values/styles.xml @@ -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> 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 b7fde12f1d22..000000000000 --- 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 000000000000..ec4c00ed6117 --- /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 = "<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); + } + +} diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java index d9bd2fcc0acb..73ff3a9b5d1e 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 8a18d7229435..000000000000 --- 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<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(); - } -} |