Default Carrier app for traffic mitigation
- have the basic function working, support traffic mitigation and
captive portal login
- support carrier customization, OEM could configure a list of carrier
actions to act upon certain signals
- unit test
Test: Manual test with live sim card & runtest --path
frameworks/base/packages/CarrierDefaultApp
Bug: 30958215
Change-Id: Ie99be3b95e8a1dd60fc51bef703836478fbde09d
diff --git a/packages/CarrierDefaultApp/Android.mk b/packages/CarrierDefaultApp/Android.mk
new file mode 100644
index 0000000..82be132
--- /dev/null
+++ b/packages/CarrierDefaultApp/Android.mk
@@ -0,0 +1,14 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CarrierDefaultApp
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
+
+# This finds and builds the test apk as well, so a single make does both.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/packages/CarrierDefaultApp/AndroidManifest.xml b/packages/CarrierDefaultApp/AndroidManifest.xml
new file mode 100644
index 0000000..28d9e5c
--- /dev/null
+++ b/packages/CarrierDefaultApp/AndroidManifest.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.carrierdefaultapp"
+ android:sharedUserId="android.uid.phone" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_INTERNAL" />
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS" />
+ <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
+
+ <application android:label="@string/app_name" >
+ <receiver android:name="com.android.carrierdefaultapp.CarrierDefaultBroadcastReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.CARRIER_SIGNAL_REDIRECTED" />
+ </intent-filter>
+ </receiver>
+ <activity android:name="com.android.carrierdefaultapp.CaptivePortalLaunchActivity"
+ android:theme="@android:style/Theme.Translucent.NoTitleBar"
+ android:excludeFromRecents="true"/>
+ </application>
+</manifest>
diff --git a/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml
new file mode 100644
index 0000000..5896757
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/drawable/ic_sim_card.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2016 Google Inc.
+
+ 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/glif_icon_size"
+ android:height="@dimen/glif_icon_size"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+ <path
+ android:fillColor="?android:attr/colorPrimary"
+ android:pathData="M39.98,8c0,-2.21 -1.77,-4 -3.98,-4L20,4L8,16v24c0,2.21 1.79,4 4,4h24.02c2.21,0 3.98,-1.79 3.98,-4l-0.02,-32zM18,38h-4v-4h4v4zM34,38h-4v-4h4v4zM18,30h-4v-8h4v8zM26,38h-4v-8h4v8zM26,26h-4v-4h4v4zM34,30h-4v-8h4v8z" />
+</vector>
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/res/values/dimens.xml b/packages/CarrierDefaultApp/res/values/dimens.xml
new file mode 100644
index 0000000..a3c5049
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="glif_icon_size">32dp</dimen>
+</resources>
diff --git a/packages/CarrierDefaultApp/res/values/strings.xml b/packages/CarrierDefaultApp/res/values/strings.xml
new file mode 100644
index 0000000..838ff39
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/values/strings.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app_name">CarrierDefaultApp</string>
+ <string name="portal_notification_id">Activate your service</string>
+ <string name="no_data_notification_id">No data service</string>
+ <string name="portal_notification_detail">Tap to activate your service</string>
+ <string name="no_data_notification_detail">No Service, please contact your service provider</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>
+</resources>
diff --git a/packages/CarrierDefaultApp/res/values/styles.xml b/packages/CarrierDefaultApp/res/values/styles.xml
new file mode 100644
index 0000000..3d26915
--- /dev/null
+++ b/packages/CarrierDefaultApp/res/values/styles.xml
@@ -0,0 +1,3 @@
+<resources>
+ <style name="AlertDialog" parent="android:Theme.Material.Light.Dialog.Alert"/>
+</resources>
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java
new file mode 100644
index 0000000..b7fde12
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CaptivePortalLaunchActivity.java
@@ -0,0 +1,233 @@
+/*
+ * 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/CarrierActionUtils.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java
new file mode 100644
index 0000000..db4890f
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierActionUtils.java
@@ -0,0 +1,175 @@
+/*
+ * 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.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.carrierdefaultapp.R;
+/**
+ * This util class provides common logic for carrier actions
+ */
+public class CarrierActionUtils {
+ private static final String TAG = CarrierActionUtils.class.getSimpleName();
+
+ private static final String PORTAL_NOTIFICATION_TAG = "CarrierDefault.Portal.Notification";
+ private static final String NO_DATA_NOTIFICATION_TAG = "CarrierDefault.NoData.Notification";
+ private static final int PORTAL_NOTIFICATION_ID = 0;
+ private static final int NO_DATA_NOTIFICATION_ID = 1;
+ private static boolean ENABLE = true;
+
+ // A list of supported carrier action idx
+ public static final int CARRIER_ACTION_ENABLE_METERED_APNS = 0;
+ public static final int CARRIER_ACTION_DISABLE_METERED_APNS = 1;
+ public static final int CARRIER_ACTION_DISABLE_RADIO = 2;
+ public static final int CARRIER_ACTION_ENABLE_RADIO = 3;
+ public static final int CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION = 4;
+ public static final int CARRIER_ACTION_SHOW_NO_DATA_SERVICE_NOTIFICATION = 5;
+ public static final int CARRIER_ACTION_CANCEL_ALL_NOTIFICATIONS = 6;
+
+ public static void applyCarrierAction(int actionIdx, Intent intent, Context context) {
+ switch (actionIdx) {
+ case CARRIER_ACTION_ENABLE_METERED_APNS:
+ onEnableAllMeteredApns(intent, context);
+ break;
+ case CARRIER_ACTION_DISABLE_METERED_APNS:
+ onDisableAllMeteredApns(intent, context);
+ break;
+ case CARRIER_ACTION_DISABLE_RADIO:
+ onDisableRadio(intent, context);
+ break;
+ case CARRIER_ACTION_ENABLE_RADIO:
+ onEnableRadio(intent, context);
+ break;
+ case CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION:
+ onShowCaptivePortalNotification(intent, context);
+ break;
+ case CARRIER_ACTION_SHOW_NO_DATA_SERVICE_NOTIFICATION:
+ onShowNoDataServiceNotification(context);
+ break;
+ case CARRIER_ACTION_CANCEL_ALL_NOTIFICATIONS:
+ onCancelAllNotifications(context);
+ break;
+ default:
+ loge("unsupported carrier action index: " + actionIdx);
+ }
+ }
+
+ private static void onDisableAllMeteredApns(Intent intent, Context context) {
+ int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+ SubscriptionManager.getDefaultVoiceSubscriptionId());
+ logd("onDisableAllMeteredApns subId: " + subId);
+ final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class);
+ telephonyMgr.carrierActionSetMeteredApnsEnabled(subId, !ENABLE);
+ }
+
+ private static void onEnableAllMeteredApns(Intent intent, Context context) {
+ int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+ SubscriptionManager.getDefaultVoiceSubscriptionId());
+ logd("onEnableAllMeteredApns subId: " + subId);
+ final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class);
+ telephonyMgr.carrierActionSetMeteredApnsEnabled(subId, ENABLE);
+ }
+
+ private static void onDisableRadio(Intent intent, Context context) {
+ int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+ SubscriptionManager.getDefaultVoiceSubscriptionId());
+ logd("onDisableRadio subId: " + subId);
+ final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class);
+ telephonyMgr.carrierActionSetRadioEnabled(subId, !ENABLE);
+ }
+
+ private static void onEnableRadio(Intent intent, Context context) {
+ int subId = intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY,
+ SubscriptionManager.getDefaultVoiceSubscriptionId());
+ logd("onEnableRadio subId: " + subId);
+ final TelephonyManager telephonyMgr = context.getSystemService(TelephonyManager.class);
+ telephonyMgr.carrierActionSetRadioEnabled(subId, ENABLE);
+ }
+
+ private static void onShowCaptivePortalNotification(Intent intent, Context context) {
+ logd("onShowCaptivePortalNotification");
+ final NotificationManager notificationMgr = context.getSystemService(
+ NotificationManager.class);
+ Intent portalIntent = new Intent(context, CaptivePortalLaunchActivity.class);
+ portalIntent.putExtras(intent);
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, portalIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ Notification notification = getNotification(context, R.string.portal_notification_id,
+ R.string.portal_notification_detail, pendingIntent);
+ try {
+ notificationMgr.notify(PORTAL_NOTIFICATION_TAG, PORTAL_NOTIFICATION_ID, notification);
+ } catch (NullPointerException npe) {
+ loge("setNotificationVisible: " + npe);
+ }
+ }
+
+ private static void onShowNoDataServiceNotification(Context context) {
+ logd("onShowNoDataServiceNotification");
+ final NotificationManager notificationMgr = context.getSystemService(
+ NotificationManager.class);
+ Notification notification = getNotification(context, R.string.no_data_notification_id,
+ R.string.no_data_notification_detail, null);
+ try {
+ notificationMgr.notify(NO_DATA_NOTIFICATION_TAG, NO_DATA_NOTIFICATION_ID, notification);
+ } catch (NullPointerException npe) {
+ loge("setNotificationVisible: " + npe);
+ }
+ }
+
+ private static void onCancelAllNotifications(Context context) {
+ logd("onCancelAllNotifications");
+ final NotificationManager notificationMgr = context.getSystemService(
+ NotificationManager.class);
+ notificationMgr.cancelAll();
+ }
+
+ private static Notification getNotification(Context context, int titleId, int textId,
+ PendingIntent pendingIntent) {
+ Resources resources = context.getResources();
+ Notification.Builder builder = new Notification.Builder(context)
+ .setContentTitle(resources.getString(titleId))
+ .setContentText(resources.getString(textId))
+ .setSmallIcon(R.drawable.ic_sim_card)
+ .setOngoing(true)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setDefaults(Notification.DEFAULT_ALL)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setLocalOnly(true)
+ .setWhen(System.currentTimeMillis())
+ .setShowWhen(false);
+
+ if (pendingIntent != null) {
+ builder.setContentIntent(pendingIntent);
+ }
+ return builder.build();
+ }
+
+ private static void logd(String s) {
+ Log.d(TAG, s);
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierDefaultBroadcastReceiver.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierDefaultBroadcastReceiver.java
new file mode 100644
index 0000000..bc0fa02
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CarrierDefaultBroadcastReceiver.java
@@ -0,0 +1,37 @@
+/*
+ * 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.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import java.util.List;
+
+public class CarrierDefaultBroadcastReceiver extends BroadcastReceiver{
+ private static final String TAG = CarrierDefaultBroadcastReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive intent: " + intent.getAction());
+ List<Integer> actionList = CustomConfigLoader.loadCarrierActionList(context, intent);
+ for (int actionIdx : actionList) {
+ Log.d(TAG, "apply carrier action idx: " + actionIdx);
+ CarrierActionUtils.applyCarrierAction(actionIdx, intent, context);
+ }
+ }
+}
diff --git a/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CustomConfigLoader.java b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CustomConfigLoader.java
new file mode 100644
index 0000000..e1125d9
--- /dev/null
+++ b/packages/CarrierDefaultApp/src/com/android/carrierdefaultapp/CustomConfigLoader.java
@@ -0,0 +1,166 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Default carrier app allows carrier customization. OEMs could configure a list
+ * of carrier actions defined in {@link com.android.carrierdefaultapp.CarrierActionUtils
+ * CarrierActionUtils} to act upon certain signal or even different args of the same signal.
+ * This allows different interpretations of the signal between carriers and could easily alter the
+ * app's behavior in a configurable way. This helper class loads and parses the carrier configs
+ * and return a list of predefined carrier actions for the given input signal.
+ */
+public class CustomConfigLoader {
+ // delimiters for parsing carrier configs of the form "arg1, arg2 : action1, action2"
+ private static final String INTRA_GROUP_DELIMITER = "\\s*,\\s*";
+ private static final String INTER_GROUP_DELIMITER = "\\s*:\\s*";
+
+ private static final String TAG = CustomConfigLoader.class.getSimpleName();
+ private static final boolean VDBG = Rlog.isLoggable(TAG, Log.VERBOSE);
+
+ /**
+ * loads and parses the carrier config, return a list of carrier action for the given signal
+ * @param context
+ * @param intent passing signal for config match
+ * @return a list of carrier action for the given signal based on the carrier config.
+ *
+ * Example: input intent TelephonyIntent.ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED
+ * This intent allows fined-grained matching based on both intent type & extra values:
+ * apnType and errorCode.
+ * apnType read from passing intent is "default" and errorCode is 0x26 for example and
+ * returned carrier config from carrier_default_actions_on_redirection_string_array is
+ * {
+ * "default, 0x26:1,4", // 0x26(NETWORK_FAILURE)
+ * "default, 0x70:2,3" // 0x70(APN_TYPE_CONFLICT)
+ * }
+ * [1, 4] // 1(CARRIER_ACTION_DISABLE_METERED_APNS), 4(CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION)
+ * returns as the action index list based on the matching rule.
+ */
+ public static List<Integer> loadCarrierActionList(Context context, Intent intent) {
+ CarrierConfigManager carrierConfigManager = (CarrierConfigManager) context.getSystemService(
+ Context.CARRIER_CONFIG_SERVICE);
+ // return an empty list if no match found
+ List<Integer> actionList = new ArrayList<>();
+ if (carrierConfigManager == null) {
+ Rlog.e(TAG, "load carrier config failure with carrier config manager uninitialized");
+ return actionList;
+ }
+ PersistableBundle b = carrierConfigManager.getConfig();
+ if (b != null) {
+ String[] configs = null;
+ // used for intents which allow fine-grained interpretation based on intent extras
+ String arg1 = null;
+ String arg2 = null;
+ switch (intent.getAction()) {
+ case TelephonyIntents.ACTION_CARRIER_SIGNAL_REDIRECTED:
+ configs = b.getStringArray(CarrierConfigManager
+ .KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY);
+ break;
+ case TelephonyIntents.ACTION_CARRIER_SIGNAL_REQUEST_NETWORK_FAILED:
+ configs = b.getStringArray(CarrierConfigManager
+ .KEY_CARRIER_DEFAULT_ACTIONS_ON_DCFAILURE_STRING_ARRAY);
+ arg1 = intent.getStringExtra(TelephonyIntents.EXTRA_APN_TYPE_KEY);
+ arg2 = intent.getStringExtra(TelephonyIntents.EXTRA_ERROR_CODE_KEY);
+ break;
+ default:
+ Rlog.e(TAG, "load carrier config failure with un-configured key: " +
+ intent.getAction());
+ break;
+ }
+ if (!ArrayUtils.isEmpty(configs)) {
+ for (String config : configs) {
+ // parse each config until find the matching one
+ matchConfig(config, arg1, arg2, actionList);
+ if (!actionList.isEmpty()) {
+ // return the first match
+ if (VDBG) Rlog.d(TAG, "found match action list: " + actionList.toString());
+ return actionList;
+ }
+ }
+ }
+ Rlog.d(TAG, "no matching entry for signal: " + intent.getAction() + "arg1: " + arg1
+ + "arg2: " + arg2);
+ }
+ return actionList;
+ }
+
+ /**
+ * Match based on the config's format and input args
+ * passing arg1, arg2 should match the format of the config
+ * case 1: config {actionIdx1, actionIdx2...} arg1 and arg2 must be null
+ * case 2: config {arg1, arg2 : actionIdx1, actionIdx2...} requires full match of non-null args
+ * case 3: config {arg1 : actionIdx1, actionIdx2...} only need to match arg1
+ *
+ * @param config action list config obtained from CarrierConfigManager
+ * @param arg1 first intent argument, set if required for config match
+ * @param arg2 second intent argument, set if required for config match
+ * @param actionList append each parsed action to the passing list
+ */
+ private static void matchConfig(String config, String arg1, String arg2,
+ List<Integer> actionList) {
+ String[] splitStr = config.trim().split(INTER_GROUP_DELIMITER, 2);
+ String actionStr = null;
+
+ if (splitStr.length == 1 && arg1 == null && arg2 == null) {
+ // case 1
+ actionStr = splitStr[0];
+ } else if (splitStr.length == 2 && arg1 != null && arg2 != null) {
+ // case 2
+ String[] args = splitStr[0].split(INTRA_GROUP_DELIMITER);
+ if (args.length == 2 && TextUtils.equals(arg1, args[0]) &&
+ TextUtils.equals(arg2, args[1])) {
+ actionStr = splitStr[1];
+ }
+ } else if ((splitStr.length == 2) && (arg1 != null) && (arg2 == null)) {
+ // case 3
+ String[] args = splitStr[0].split(INTRA_GROUP_DELIMITER);
+ if (args.length == 1 && TextUtils.equals(arg1, args[0])) {
+ actionStr = splitStr[1];
+ }
+ }
+ // convert from string -> action idx list if found a matching entry
+ String[] actions = null;
+ if (!TextUtils.isEmpty(actionStr)) {
+ actions = actionStr.split(INTRA_GROUP_DELIMITER);
+ }
+ if (!ArrayUtils.isEmpty(actions)) {
+ for (String idx : actions) {
+ try {
+ actionList.add(Integer.parseInt(idx));
+ } catch (NumberFormatException e) {
+ Rlog.e(TAG, "NumberFormatException(string: " + idx + " config:" + config + "): "
+ + e);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/CarrierDefaultApp/tests/Android.mk b/packages/CarrierDefaultApp/tests/Android.mk
new file mode 100644
index 0000000..6ebb575
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/Android.mk
@@ -0,0 +1,25 @@
+# Copyright 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_CERTIFICATE := platform
+
+# Include all makefiles in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
+
+
+
+
diff --git a/packages/CarrierDefaultApp/tests/unit/Android.mk b/packages/CarrierDefaultApp/tests/unit/Android.mk
new file mode 100644
index 0000000..092df50
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/Android.mk
@@ -0,0 +1,34 @@
+# Copyright 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := platform
+
+LOCAL_JAVA_LIBRARIES := android.test.runner telephony-common
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test mockito-target-minus-junit4
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CarrierDefaultAppUnitTests
+
+LOCAL_INSTRUMENTATION_FOR := CarrierDefaultApp
+
+include $(BUILD_PACKAGE)
+
diff --git a/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..3a06a09
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.carrierdefaultapp.tests.unit">
+ <uses-permission android:name="android.permission.GET_INTENT_SENDER_INTENT" />
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.carrierdefaultapp"
+ android:label="CarrierDefaultApp Unit Test Cases">
+ </instrumentation>
+</manifest>
+
+
+
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultActivityTestCase.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultActivityTestCase.java
new file mode 100644
index 0000000..c5a1243
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultActivityTestCase.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.test.ActivityUnitTestCase;
+import android.util.Log;
+
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+
+public class CarrierDefaultActivityTestCase<T extends Activity> extends ActivityUnitTestCase<T> {
+
+ protected TestContext mContext;
+
+ private T mActivity;
+
+ CarrierDefaultActivityTestCase(Class<T> activityClass) {
+ super(activityClass);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext(getInstrumentation().getTargetContext());
+ setActivityContext(mContext);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ protected T startActivity() throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity = startActivity(createActivityIntent(), null, null);
+ }
+ });
+ return mActivity;
+ }
+
+ protected void stopActivity() throws Exception {
+ getInstrumentation().callActivityOnStop(mActivity);
+ }
+
+ protected Intent createActivityIntent() {
+ Intent intent = new Intent();
+ return intent;
+ }
+
+ protected <S> void injectSystemService(Class<S> cls, S service) {
+ mContext.injectSystemService(cls, service);
+ }
+}
\ No newline at end of file
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultReceiverTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultReceiverTest.java
new file mode 100644
index 0000000..f9dbcd4
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/CarrierDefaultReceiverTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.PersistableBundle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
+import android.test.InstrumentationTestCase;
+
+import com.android.internal.telephony.PhoneConstants;
+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 org.mockito.MockitoAnnotations;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class CarrierDefaultReceiverTest extends InstrumentationTestCase {
+ @Mock
+ private NotificationManager mNotificationMgr;
+ @Mock
+ private TelephonyManager mTelephonyMgr;
+ @Mock
+ private CarrierConfigManager mCarrierConfigMgr;
+ @Captor
+ private ArgumentCaptor<Integer> mInt;
+ @Captor
+ private ArgumentCaptor<Notification> mNotification;
+ @Captor
+ private ArgumentCaptor<String> mString;
+ private TestContext mContext;
+ private CarrierDefaultBroadcastReceiver mReceiver;
+ private static String TAG;
+
+ private static final String PORTAL_NOTIFICATION_TAG = "CarrierDefault.Portal.Notification";
+ private static final int PORTAL_NOTIFICATION_ID = 0;
+ private static int subId = 0;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ TAG = this.getClass().getSimpleName();
+ MockitoAnnotations.initMocks(this);
+ mContext = new TestContext(getInstrumentation().getTargetContext());
+ mContext.injectSystemService(NotificationManager.class, mNotificationMgr);
+ mContext.injectSystemService(TelephonyManager.class, mTelephonyMgr);
+ mContext.injectSystemService(CarrierConfigManager.class, mCarrierConfigMgr);
+
+ mReceiver = new CarrierDefaultBroadcastReceiver();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testOnReceiveRedirection() {
+ // carrier action idx list includes 4(portal notification) & 1(disable metered APNs)
+ // upon redirection signal
+ PersistableBundle b = new PersistableBundle();
+ b.putStringArray(CarrierConfigManager
+ .KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY, new String[]{"4,1"});
+ doReturn(b).when(mCarrierConfigMgr).getConfig();
+
+ Intent intent = new Intent(TelephonyIntents.ACTION_CARRIER_SIGNAL_REDIRECTED);
+ intent.putExtra(PhoneConstants.SUBSCRIPTION_KEY, subId);
+ Rlog.d(TAG, "OnReceive redirection intent");
+ mReceiver.onReceive(mContext, intent);
+
+ mContext.waitForMs(100);
+
+ Rlog.d(TAG, "verify carrier action: showPortalNotification");
+ verify(mNotificationMgr, times(1)).notify(mString.capture(), mInt.capture(),
+ mNotification.capture());
+ assertEquals(PORTAL_NOTIFICATION_ID, (int) mInt.getValue());
+ assertEquals(PORTAL_NOTIFICATION_TAG, mString.getValue());
+ PendingIntent pendingIntent = mNotification.getValue().contentIntent;
+ assertNotNull(pendingIntent);
+
+ Rlog.d(TAG, "verify carrier action: disable all metered apns");
+ verify(mTelephonyMgr).carrierActionSetMeteredApnsEnabled(eq(subId), eq(false));
+ }
+}
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java
new file mode 100644
index 0000000..8a18d72
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/LaunchCaptivePortalActivityTest.java
@@ -0,0 +1,108 @@
+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();
+ }
+}
diff --git a/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/TestContext.java b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/TestContext.java
new file mode 100644
index 0000000..e50666b
--- /dev/null
+++ b/packages/CarrierDefaultApp/tests/unit/src/com/android/carrierdefaultapp/TestContext.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 Google Inc.
+ *
+ * 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.content.Context;
+import android.content.ContextWrapper;
+import android.util.Log;
+
+import java.util.HashMap;
+
+public class TestContext extends ContextWrapper {
+
+ private final String TAG = this.getClass().getSimpleName();
+
+ private HashMap<String, Object> mInjectedSystemServices = new HashMap<>();
+
+ public TestContext(Context base) {
+ super(base);
+ }
+
+ public <S> void injectSystemService(Class<S> cls, S service) {
+ final String name = getSystemServiceName(cls);
+ mInjectedSystemServices.put(name, service);
+ }
+
+ @Override
+ public Context getApplicationContext() {
+ return this;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (mInjectedSystemServices.containsKey(name)) {
+ Log.d(TAG, "return mocked system service for " + name);
+ return mInjectedSystemServices.get(name);
+ }
+ Log.d(TAG, "return real system service for " + name);
+ return super.getSystemService(name);
+ }
+
+ public static void waitForMs(long ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ }
+ }
+}
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 4565dbd..36e0c71 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -843,6 +843,58 @@
"signal_pco_receiver_string_array";
/**
+ * Defines carrier-specific actions which act upon
+ * android.intent.action.CARRIER_SIGNAL_REDIRECTED, used for customization of the
+ * default carrier app
+ * Format: "CARRIER_ACTION_IDX, ..."
+ * Where {@code CARRIER_ACTION_IDX} is an integer defined in
+ * {@link com.android.carrierdefaultapp.CarrierActionUtils CarrierActionUtils}
+ * Example:
+ * {@link com.android.carrierdefaultapp.CarrierActionUtils#CARRIER_ACTION_DISABLE_METERED_APNS
+ * disable_metered_apns}
+ * @hide
+ */
+ public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY =
+ "carrier_default_actions_on_redirection_string_array";
+
+ /**
+ * Defines carrier-specific actions which act upon
+ * android.intent.action.CARRIER_SIGNAL_REQUEST_NETWORK_FAILED
+ * and configured signal args:
+ * {@link com.android.internal.telephony.TelephonyIntents#EXTRA_APN_TYPE_KEY apnType},
+ * {@link com.android.internal.telephony.TelephonyIntents#EXTRA_ERROR_CODE_KEY errorCode}
+ * used for customization of the default carrier app
+ * Format:
+ * {
+ * "APN_1, ERROR_CODE_1 : CARRIER_ACTION_IDX_1, CARRIER_ACTION_IDX_2...",
+ * "APN_1, ERROR_CODE_2 : CARRIER_ACTION_IDX_1 "
+ * }
+ * Where {@code APN_1} is a string defined in
+ * {@link com.android.internal.telephony.PhoneConstants PhoneConstants}
+ * Example: "default"
+ *
+ * {@code ERROR_CODE_1} is an integer defined in
+ * {@link com.android.internal.telephony.dataconnection.DcFailCause DcFailure}
+ * Example:
+ * {@link com.android.internal.telephony.dataconnection.DcFailCause#MISSING_UNKNOWN_APN}
+ *
+ * {@code CARRIER_ACTION_IDX_1} is an integer defined in
+ * {@link com.android.carrierdefaultapp.CarrierActionUtils CarrierActionUtils}
+ * Example:
+ * {@link com.android.carrierdefaultapp.CarrierActionUtils#CARRIER_ACTION_DISABLE_METERED_APNS}
+ * @hide
+ */
+ public static final String KEY_CARRIER_DEFAULT_ACTIONS_ON_DCFAILURE_STRING_ARRAY =
+ "carrier_default_actions_on_dcfailure_string_array";
+
+ /**
+ * Defines a list of acceptable redirection url for default carrier app
+ * @hides
+ */
+ public static final String KEY_CARRIER_DEFAULT_REDIRECTION_URL_STRING_ARRAY =
+ "carrier_default_redirection_url_string_array";
+
+ /**
* Determines whether the carrier supports making non-emergency phone calls while the phone is
* in emergency callback mode. Default value is {@code true}, meaning that non-emergency calls
* are allowed in emergency callback mode.
@@ -1221,6 +1273,15 @@
sDefaults.putStringArray(KEY_SIGNAL_PCO_RECEIVER_STRING_ARRAY, null);
sDefaults.putString(KEY_CARRIER_SETUP_APP_STRING, "");
+ // Default carrier app configurations
+ sDefaults.putStringArray(KEY_CARRIER_DEFAULT_ACTIONS_ON_REDIRECTION_STRING_ARRAY,
+ new String[]{
+ "4, 1"
+ //4: CARRIER_ACTION_DISABLE_METERED_APNS
+ //1: CARRIER_ACTION_SHOW_PORTAL_NOTIFICATION
+ });
+ sDefaults.putStringArray(KEY_CARRIER_DEFAULT_REDIRECTION_URL_STRING_ARRAY, null);
+
// Rat families: {GPRS, EDGE}, {EVDO, EVDO_A, EVDO_B}, {UMTS, HSPA, HSDPA, HSUPA, HSPAP},
// {LTE, LTE_CA}
// Order is important - lowest precidence first