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