Merge "Default Carrier app for traffic mitigation"

am: 4533b1ccdd

Change-Id: I999285660c3a53fb2da8f18ddde9042f611f4e96
This commit is contained in:
Chen Xu
2017-01-13 02:28:28 +00:00
committed by android-build-merger
18 changed files with 1215 additions and 0 deletions

View File

@@ -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))

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="glif_icon_size">32dp</dimen>
</resources>

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
<resources>
<style name="AlertDialog" parent="android:Theme.Material.Light.Dialog.Alert"/>
</resources>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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))

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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) {
}
}
}

View File

@@ -842,6 +842,58 @@ public class CarrierConfigManager {
public static final String KEY_SIGNAL_PCO_RECEIVER_STRING_ARRAY =
"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
@@ -1229,6 +1281,15 @@ public class CarrierConfigManager {
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