主界面源码
ApnSettings.java 源码(Settings应用)
packages/apps/Settings/src/com/android/settings/network/apn/ApnSettings.java

/*
* Copyright (C) 2008 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.settings.network.apn;
import static com.android.settings.network.apn.ApnEditPageProviderKt.INSERT_URL;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Telephony;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.data.ApnSetting;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import com.android.settings.R;
import com.android.settings.RestrictedSettingsFragment;
import com.android.settings.flags.Flags;
import com.android.settings.network.telephony.SubscriptionRepository;
import com.android.settings.spa.SpaActivity;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import kotlin.Unit;
import java.util.ArrayList;
/** Handle each different apn setting. */
public class ApnSettings extends RestrictedSettingsFragment
implements Preference.OnPreferenceChangeListener {
static final String TAG = "ApnSettings";
public static final String APN_ID = "apn_id";
public static final String APN_LIST = "apn_list";
public static final String SUB_ID = "sub_id";
public static final String MVNO_TYPE = "mvno_type";
public static final String MVNO_MATCH_DATA = "mvno_match_data";
private static final String[] CARRIERS_PROJECTION = new String[] {
Telephony.Carriers._ID,
Telephony.Carriers.NAME,
Telephony.Carriers.APN,
Telephony.Carriers.TYPE,
Telephony.Carriers.MVNO_TYPE,
Telephony.Carriers.MVNO_MATCH_DATA,
Telephony.Carriers.EDITED_STATUS,
};
private static final int ID_INDEX = 0;
private static final int NAME_INDEX = 1;
private static final int APN_INDEX = 2;
private static final int TYPES_INDEX = 3;
private static final int MVNO_TYPE_INDEX = 4;
private static final int MVNO_MATCH_DATA_INDEX = 5;
private static final int EDITED_INDEX = 6;
private static final int MENU_NEW = Menu.FIRST;
private static final int MENU_RESTORE = Menu.FIRST + 1;
private static final int DIALOG_RESTORE_DEFAULTAPN = 1001;
private boolean mRestoreDefaultApnMode;
private UserManager mUserManager;
private int mSubId;
private PreferredApnRepository mPreferredApnRepository;
@Nullable
private String mPreferredApnKey;
private String mMvnoType;
private String mMvnoMatchData;
private boolean mUnavailable;
private boolean mHideImsApn;
private boolean mAllowAddingApns;
private boolean mHidePresetApnDetails;
public ApnSettings() {
super(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.APN;
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
final Activity activity = getActivity();
mSubId = activity.getIntent().getIntExtra(SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mPreferredApnRepository = new PreferredApnRepository(activity, mSubId);
setIfOnlyAvailableForAdmins(true);
final CarrierConfigManager configManager = (CarrierConfigManager)
getSystemService(Context.CARRIER_CONFIG_SERVICE);
final PersistableBundle b = configManager.getConfigForSubId(mSubId);
mHideImsApn = b.getBoolean(CarrierConfigManager.KEY_HIDE_IMS_APN_BOOL);
mAllowAddingApns = b.getBoolean(CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL);
if (mAllowAddingApns) {
final String[] readOnlyApnTypes = b.getStringArray(
CarrierConfigManager.KEY_READ_ONLY_APN_TYPES_STRING_ARRAY);
// if no apn type can be edited, do not allow adding APNs
if (ApnEditor.hasAllApns(readOnlyApnTypes)) {
Log.d(TAG, "not allowing adding APN because all APN types are read only");
mAllowAddingApns = false;
}
}
mHidePresetApnDetails = b.getBoolean(CarrierConfigManager.KEY_HIDE_PRESET_APN_DETAILS_BOOL);
mUserManager = UserManager.get(activity);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getEmptyTextView().setText(com.android.settingslib.R.string.apn_settings_not_available);
mUnavailable = isUiRestricted();
setHasOptionsMenu(!mUnavailable);
if (mUnavailable) {
addPreferencesFromResource(R.xml.placeholder_prefs);
return;
}
addPreferencesFromResource(R.xml.apn_settings);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
new SubscriptionRepository(requireContext())
.collectSubscriptionEnabled(mSubId, viewLifecycleOwner, (isEnabled) -> {
if (!isEnabled) {
Log.d(TAG, "Due to subscription not enabled, closes APN settings page");
finish();
}
return Unit.INSTANCE;
});
mPreferredApnRepository.collectPreferredApn(viewLifecycleOwner, (preferredApn) -> {
mPreferredApnKey = preferredApn;
final PreferenceGroup apnPreferenceList = findPreference(APN_LIST);
for (int i = 0; i < apnPreferenceList.getPreferenceCount(); i++) {
ApnPreference apnPreference = (ApnPreference) apnPreferenceList.getPreference(i);
apnPreference.setIsChecked(apnPreference.getKey().equals(preferredApn));
}
return Unit.INSTANCE;
});
}
@Override
public void onResume() {
super.onResume();
if (mUnavailable) {
return;
}
if (!mRestoreDefaultApnMode) {
fillList();
}
}
@Override
public EnforcedAdmin getRestrictionEnforcedAdmin() {
final UserHandle user = UserHandle.of(mUserManager.getProcessUserId());
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS, user)
&& !mUserManager.hasBaseUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS,
user)) {
return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN;
}
return null;
}
private void fillList() {
final Uri simApnUri = Uri.withAppendedPath(Telephony.Carriers.SIM_APN_URI,
String.valueOf(mSubId));
final StringBuilder where =
new StringBuilder("NOT (type='ia' AND (apn=\"\" OR apn IS NULL)) AND "
+ "user_visible!=0");
// Remove Emergency type, users should not mess with that
where.append(" AND NOT (type='emergency')");
if (mHideImsApn) {
where.append(" AND NOT (type='ims')");
}
final Cursor cursor = getContentResolver().query(simApnUri,
CARRIERS_PROJECTION, where.toString(), null,
Telephony.Carriers.DEFAULT_SORT_ORDER);
if (cursor != null) {
final PreferenceGroup apnPrefList = findPreference(APN_LIST);
apnPrefList.removeAll();
final ArrayList<ApnPreference> apnList = new ArrayList<ApnPreference>();
final ArrayList<ApnPreference> mmsApnList = new ArrayList<ApnPreference>();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
final String name = cursor.getString(NAME_INDEX);
final String apn = cursor.getString(APN_INDEX);
final String key = cursor.getString(ID_INDEX);
final String type = cursor.getString(TYPES_INDEX);
final int edited = cursor.getInt(EDITED_INDEX);
mMvnoType = cursor.getString(MVNO_TYPE_INDEX);
mMvnoMatchData = cursor.getString(MVNO_MATCH_DATA_INDEX);
final ApnPreference pref = new ApnPreference(getPrefContext());
pref.setKey(key);
pref.setTitle(name);
pref.setPersistent(false);
pref.setOnPreferenceChangeListener(this);
pref.setSubId(mSubId);
if (mHidePresetApnDetails && edited == Telephony.Carriers.UNEDITED) {
pref.setHideDetails();
} else {
pref.setSummary(apn);
}
final boolean defaultSelectable =
((type == null) || type.contains(ApnSetting.TYPE_DEFAULT_STRING));
pref.setDefaultSelectable(defaultSelectable);
if (defaultSelectable) {
pref.setIsChecked(key.equals(mPreferredApnKey));
apnList.add(pref);
} else {
mmsApnList.add(pref);
}
cursor.moveToNext();
}
cursor.close();
for (Preference preference : apnList) {
apnPrefList.addPreference(preference);
}
for (Preference preference : mmsApnList) {
apnPrefList.addPreference(preference);
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!mUnavailable) {
if (mAllowAddingApns) {
menu.add(0, MENU_NEW, 0,
getResources().getString(R.string.menu_new))
.setIcon(R.drawable.ic_add_24dp)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
menu.add(0, MENU_RESTORE, 0,
getResources().getString(R.string.menu_restore))
.setIcon(android.R.drawable.ic_menu_upload);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_NEW:
addNewApn();
return true;
case MENU_RESTORE:
restoreDefaultApn();
return true;
}
return super.onOptionsItemSelected(item);
}
private void addNewApn() {
if (Flags.newApnPageEnabled()) {
String route = ApnEditPageProvider.INSTANCE.getRoute(
INSERT_URL, Telephony.Carriers.CONTENT_URI, mSubId);
SpaActivity.startSpaActivity(getContext(), route);
} else {
final Intent intent = new Intent(Intent.ACTION_INSERT, Telephony.Carriers.CONTENT_URI);
intent.putExtra(SUB_ID, mSubId);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
if (!TextUtils.isEmpty(mMvnoType) && !TextUtils.isEmpty(mMvnoMatchData)) {
intent.putExtra(MVNO_TYPE, mMvnoType);
intent.putExtra(MVNO_MATCH_DATA, mMvnoMatchData);
}
startActivity(intent);
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
Log.d(TAG, "onPreferenceChange(): Preference - " + preference
+ ", newValue - " + newValue + ", newValue type - "
+ newValue.getClass());
if (newValue instanceof String) {
mPreferredApnRepository.setPreferredApn((String) newValue);
}
return true;
}
private void restoreDefaultApn() {
showDialog(DIALOG_RESTORE_DEFAULTAPN);
mRestoreDefaultApnMode = true;
mPreferredApnRepository.restorePreferredApn(getViewLifecycleOwner(), () -> {
onPreferredApnRestored();
return Unit.INSTANCE;
});
}
private void onPreferredApnRestored() {
final Activity activity = getActivity();
if (activity == null) {
mRestoreDefaultApnMode = false;
return;
}
fillList();
getPreferenceScreen().setEnabled(true);
mRestoreDefaultApnMode = false;
removeDialog(DIALOG_RESTORE_DEFAULTAPN);
Toast.makeText(
activity,
getResources().getString(R.string.restore_default_apn_completed),
Toast.LENGTH_LONG).show();
}
@Override
public Dialog onCreateDialog(int id) {
if (id == DIALOG_RESTORE_DEFAULTAPN) {
final ProgressDialog dialog = new ProgressDialog(getActivity()) {
public boolean onTouchEvent(MotionEvent event) {
return true;
}
};
dialog.setMessage(getResources().getString(R.string.restore_default_apn));
dialog.setCancelable(false);
return dialog;
}
return null;
}
@Override
public int getDialogMetricsCategory(int dialogId) {
if (dialogId == DIALOG_RESTORE_DEFAULTAPN) {
return SettingsEnums.DIALOG_APN_RESTORE_DEFAULT;
}
return 0;
}
}
变量和API列表图
- F标识静态常量,空白F是private或package-private,填充F是公开public的。

- M标识方法接口,紫色填充M是 public ,无填充紫色M是 private 。

API 接口汇总列表
需要评估单元测试可行性,即接口可测试性。
Note:以下列出AOSP源码定义的接口,不含OEM定制的逻辑实现。
Annotation | Function method | API | 功能 | Num |
---|---|---|---|---|
/ | public ApnSettings() | ApnSettings | 构造方法 | 1 |
@Override | public int getMetricsCategory() | getMetricsCategory | 2 | |
@Override | public void onCreate(Bundle icicle) | onCreate | 界面构建 | 3 |
@Override | public void onActivityCreated(Bundle savedInstanceState) | onActivityCreated | 4 | |
@Override | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) | onViewCreated | 5 | |
@Override | public void onResume() | onResume | 6 | |
@Override | public EnforcedAdmin getRestrictionEnforcedAdmin() | getRestrictionEnforcedAdmin | 7 | |
/ | private void fillList() | fillList | 查询数据库,加载APN列表 | 8 |
@Override | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) | onCreateOptionsMenu | 9 | |
@Override | public boolean onOptionsItemSelected(MenuItem item) | onOptionsItemSelected | 菜单MENU_NEW MENU_RESTORE | 10 |
/ | private void addNewApn() | addNewApn | 添加APN,跳转ApnEditor | 11 |
@Override | public boolean onPreferenceChange(Preference preference, Object newValue) | onPreferenceChange | 12 | |
/ | private void restoreDefaultApn() | restoreDefaultApn | 重置APN | 13 |
/ | private void onPreferredApnRestored() | onPreferredApnRestored | 14 | |
@Override | public Dialog onCreateDialog(int id) | onCreateDialog | 15 | |
@Override | public int getDialogMetricsCategory(int dialogId) | getDialogMetricsCategory | 16 |
APN 设置 UI 启动流程
- onCreate +
- onCreateView: rootView=
- onActivityCreated +
- onResume: mUnavailable=true
- updateState: apnPrefCount==0
- ApnSettingsActivity: finish
- onPause: mUnavailable=true
- onDestroy: mUnavailable=true
ApnSettingsActivity.java 源码(MMS 应用)
在MMS应用及服务中也有ApnSettings相关的代码实现
- packages/services/Mms/src/com/android/mms/service/ApnSettings.java
- packages/apps/Messaging/src/com/android/messaging/ui/appsettings/ApnSettingsActivity.java
/*
* Copyright (C) 2015 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.messaging.ui.appsettings;
import android.app.Activity;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.UserManager;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.preference.PreferenceScreen;
import android.provider.Telephony;
import androidx.core.app.NavUtils;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.ApnDatabase;
import com.android.messaging.sms.BugleApnSettingsLoader;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
public class ApnSettingsActivity extends BugleActionBarActivity {
private static final int DIALOG_RESTORE_DEFAULTAPN = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Display the fragment as the main content.
final ApnSettingsFragment fragment = new ApnSettingsFragment();
fragment.setSubId(getIntent().getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID,
ParticipantData.DEFAULT_SELF_SUB_ID));
getFragmentManager().beginTransaction()
.replace(android.R.id.content, fragment)
.commit();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id == DIALOG_RESTORE_DEFAULTAPN) {
ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage(getResources().getString(R.string.restore_default_apn));
dialog.setCancelable(false);
return dialog;
}
return null;
}
public static class ApnSettingsFragment extends PreferenceFragment implements
Preference.OnPreferenceChangeListener {
public static final String EXTRA_POSITION = "position";
public static final String APN_ID = "apn_id";
private static final String[] APN_PROJECTION = {
Telephony.Carriers._ID, // 0
Telephony.Carriers.NAME, // 1
Telephony.Carriers.APN, // 2
Telephony.Carriers.TYPE // 3
};
private static final int ID_INDEX = 0;
private static final int NAME_INDEX = 1;
private static final int APN_INDEX = 2;
private static final int TYPES_INDEX = 3;
private static final int MENU_NEW = Menu.FIRST;
private static final int MENU_RESTORE = Menu.FIRST + 1;
private static final int EVENT_RESTORE_DEFAULTAPN_START = 1;
private static final int EVENT_RESTORE_DEFAULTAPN_COMPLETE = 2;
private static boolean mRestoreDefaultApnMode;
private RestoreApnUiHandler mRestoreApnUiHandler;
private RestoreApnProcessHandler mRestoreApnProcessHandler;
private HandlerThread mRestoreDefaultApnThread;
private String mSelectedKey;
private static final ContentValues sCurrentNullMap;
private static final ContentValues sCurrentSetMap;
private UserManager mUm;
private boolean mUnavailable;
private int mSubId;
static {
sCurrentNullMap = new ContentValues(1);
sCurrentNullMap.putNull(Telephony.Carriers.CURRENT);
sCurrentSetMap = new ContentValues(1);
sCurrentSetMap.put(Telephony.Carriers.CURRENT, "2"); // 2 for user-selected APN,
// 1 for Bugle-selected APN
}
private SQLiteDatabase mDatabase;
public void setSubId(final int subId) {
mSubId = subId;
}
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mDatabase = ApnDatabase.getApnDatabase().getWritableDatabase();
if (OsUtil.isAtLeastL()) {
mUm = (UserManager) getActivity().getSystemService(Context.USER_SERVICE);
if (!mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
setHasOptionsMenu(true);
}
} else {
setHasOptionsMenu(true);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final ListView lv = (ListView) getView().findViewById(android.R.id.list);
TextView empty = (TextView) getView().findViewById(android.R.id.empty);
if (empty != null) {
empty.setText(R.string.apn_settings_not_available);
lv.setEmptyView(empty);
}
if (OsUtil.isAtLeastL() &&
mUm.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
mUnavailable = true;
setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getActivity()));
return;
}
addPreferencesFromResource(R.xml.apn_settings);
lv.setItemsCanFocus(true);
}
@Override
public void onResume() {
super.onResume();
if (mUnavailable) {
return;
}
if (!mRestoreDefaultApnMode) {
fillList();
}
}
@Override
public void onPause() {
super.onPause();
if (mUnavailable) {
return;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mRestoreDefaultApnThread != null) {
mRestoreDefaultApnThread.quit();
}
}
private void fillList() {
final String mccMnc = PhoneUtils.getMccMncString(PhoneUtils.get(mSubId).getMccMnc());
new AsyncTask<Void, Void, Cursor>() {
@Override
protected Cursor doInBackground(Void... params) {
String selection = Telephony.Carriers.NUMERIC + " =?";
String[] selectionArgs = new String[]{ mccMnc };
final Cursor cursor = mDatabase.query(ApnDatabase.APN_TABLE, APN_PROJECTION,
selection, selectionArgs, null, null, null, null);
return cursor;
}
@Override
protected void onPostExecute(Cursor cursor) {
if (cursor != null) {
try {
PreferenceGroup apnList = (PreferenceGroup)
findPreference(getString(R.string.apn_list_pref_key));
apnList.removeAll();
mSelectedKey = BugleApnSettingsLoader.getFirstTryApn(mDatabase, mccMnc);
while (cursor.moveToNext()) {
String name = cursor.getString(NAME_INDEX);
String apn = cursor.getString(APN_INDEX);
String key = cursor.getString(ID_INDEX);
String type = cursor.getString(TYPES_INDEX);
if (BugleApnSettingsLoader.isValidApnType(type,
BugleApnSettingsLoader.APN_TYPE_MMS)) {
ApnPreference pref = new ApnPreference(getActivity());
pref.setKey(key);
pref.setTitle(name);
pref.setSummary(apn);
pref.setPersistent(false);
pref.setOnPreferenceChangeListener(ApnSettingsFragment.this);
pref.setSelectable(true);
// Turn on the radio button for the currently selected APN. If
// there is no selected APN, don't select an APN.
if ((mSelectedKey != null && mSelectedKey.equals(key))) {
pref.setChecked();
}
apnList.addPreference(pref);
}
}
} finally {
cursor.close();
}
}
}
}.execute((Void) null);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!mUnavailable) {
menu.add(0, MENU_NEW, 0,
getResources().getString(R.string.menu_new_apn))
.setIcon(R.drawable.ic_add_white)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
menu.add(0, MENU_RESTORE, 0,
getResources().getString(R.string.menu_restore_default_apn))
.setIcon(android.R.drawable.ic_menu_upload);
}
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_NEW:
addNewApn();
return true;
case MENU_RESTORE:
restoreDefaultApn();
return true;
}
return super.onOptionsItemSelected(item);
}
private void addNewApn() {
startActivity(UIIntents.get().getApnEditorIntent(getActivity(), null, mSubId));
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference) {
startActivity(
UIIntents.get().getApnEditorIntent(getActivity(), preference.getKey(), mSubId));
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue instanceof String) {
setSelectedApnKey((String) newValue);
}
return true;
}
// current=2 means user selected APN
private static final String UPDATE_SELECTION = Telephony.Carriers.CURRENT + " =?";
private static final String[] UPDATE_SELECTION_ARGS = new String[] { "2" };
private void setSelectedApnKey(final String key) {
mSelectedKey = key;
// Make database changes not on the UI thread
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// null out the previous "current=2" APN
mDatabase.update(ApnDatabase.APN_TABLE, sCurrentNullMap,
UPDATE_SELECTION, UPDATE_SELECTION_ARGS);
// set the new "current" APN (2)
String selection = Telephony.Carriers._ID + " =?";
String[] selectionArgs = new String[]{ key };
mDatabase.update(ApnDatabase.APN_TABLE, sCurrentSetMap,
selection, selectionArgs);
return null;
}
}.execute((Void) null);
}
private boolean restoreDefaultApn() {
getActivity().showDialog(DIALOG_RESTORE_DEFAULTAPN);
mRestoreDefaultApnMode = true;
if (mRestoreApnUiHandler == null) {
mRestoreApnUiHandler = new RestoreApnUiHandler();
}
if (mRestoreApnProcessHandler == null ||
mRestoreDefaultApnThread == null) {
mRestoreDefaultApnThread = new HandlerThread(
"Restore default APN Handler: Process Thread");
mRestoreDefaultApnThread.start();
mRestoreApnProcessHandler = new RestoreApnProcessHandler(
mRestoreDefaultApnThread.getLooper(), mRestoreApnUiHandler);
}
mRestoreApnProcessHandler.sendEmptyMessage(EVENT_RESTORE_DEFAULTAPN_START);
return true;
}
private class RestoreApnUiHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_RESTORE_DEFAULTAPN_COMPLETE:
fillList();
getPreferenceScreen().setEnabled(true);
mRestoreDefaultApnMode = false;
final Activity activity = getActivity();
activity.dismissDialog(DIALOG_RESTORE_DEFAULTAPN);
Toast.makeText(activity, getResources().getString(
R.string.restore_default_apn_completed), Toast.LENGTH_LONG)
.show();
break;
}
}
}
private class RestoreApnProcessHandler extends Handler {
private Handler mCachedRestoreApnUiHandler;
public RestoreApnProcessHandler(Looper looper, Handler restoreApnUiHandler) {
super(looper);
this.mCachedRestoreApnUiHandler = restoreApnUiHandler;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_RESTORE_DEFAULTAPN_START:
ApnDatabase.forceBuildAndLoadApnTables();
mCachedRestoreApnUiHandler.sendEmptyMessage(
EVENT_RESTORE_DEFAULTAPN_COMPLETE);
break;
}
}
}
}
}
测试方案
ApnPreferenceController
MobileNetwork界面入口中APN的菜单受 ApnPreferenceController 逻辑控制。
代码接口 | 功能描述 | @Test 测试方法 | 预期结果 |
---|---|---|---|
getAvailabilityStatus | 检查GSM/CDMA设备类型,确定是否隐藏network,表明菜单可用性 | getAvailabilityStatus_apnSettingsNotSupported_returnUnavailable() | 不支持apn设置,返回不可用 |
getAvailabilityStatus_apnSettingsSupportedWithCDMA_returnAvailable() | 支持CDMA,显示APN菜单 | ||
getAvailabilityStatus_carrierConfigNull_returnUnavailable() | cc空,菜单不可用 | ||
getAvailabilityStatus_hideCarrierNetworkSettings_returnUnavailable() | 当隐藏网络设置时,APN菜单置为不可用。 | ||
handlePreferenceTreeClick | 点击跳转APN列表界面 | handPreferenceTreeClick_fireIntent() |
ApnPreferenceController 源码
packages/apps/Settings/src/com/android/settings/network/telephony/ApnPreferenceController.java
- 标注@VisibleForTesting的有:setPreference 接口和 mCarrierConfigCache 全局变量。
- 定义内部私有类DpcApnEnforcedObserver
/*
* Copyright (C) 2018 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.settings.network.telephony;
import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.PersistableBundle;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceScreen;
import com.android.settings.SettingsActivity;
import com.android.settings.network.CarrierConfigCache;
import com.android.settings.network.apn.ApnSettings;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnStart;
import com.android.settingslib.core.lifecycle.events.OnStop;
/**
* Preference controller for "Apn settings"
*/
public class ApnPreferenceController extends TelephonyBasePreferenceController implements
LifecycleObserver, OnStart, OnStop {
@VisibleForTesting
CarrierConfigCache mCarrierConfigCache;
private Preference mPreference;
private DpcApnEnforcedObserver mDpcApnEnforcedObserver;
public ApnPreferenceController(Context context, String key) {
super(context, key);
mCarrierConfigCache = CarrierConfigCache.getInstance(context);
mDpcApnEnforcedObserver = new DpcApnEnforcedObserver(new Handler(Looper.getMainLooper()));
}
@Override
public int getAvailabilityStatus(int subId) {
final PersistableBundle carrierConfig = mCarrierConfigCache.getConfigForSubId(subId);
final boolean isCdmaApn = MobileNetworkUtils.isCdmaOptions(mContext, subId)
&& carrierConfig != null
&& carrierConfig.getBoolean(CarrierConfigManager.KEY_SHOW_APN_SETTING_CDMA_BOOL);
final boolean isGsmApn = MobileNetworkUtils.isGsmOptions(mContext, subId)
&& carrierConfig != null
&& carrierConfig.getBoolean(CarrierConfigManager.KEY_APN_EXPAND_BOOL);
final boolean hideCarrierNetwork = carrierConfig == null
|| carrierConfig.getBoolean(
CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL);
return !hideCarrierNetwork && (isCdmaApn || isGsmApn)
? AVAILABLE
: CONDITIONALLY_UNAVAILABLE;
}
@Override
public void onStart() {
mDpcApnEnforcedObserver.register(mContext);
}
@Override
public void onStop() {
mDpcApnEnforcedObserver.unRegister(mContext);
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mPreference = screen.findPreference(getPreferenceKey());
}
@Override
public void updateState(Preference preference) {
super.updateState(preference);
if (mPreference == null) {
return;
}
((RestrictedPreference) mPreference).setDisabledByAdmin(
MobileNetworkUtils.isDpcApnEnforced(mContext)
? RestrictedLockUtilsInternal.getDeviceOwner(mContext)
: null);
}
@Override
public boolean handlePreferenceTreeClick(Preference preference) {
if (getPreferenceKey().equals(preference.getKey())) {
// This activity runs in phone process, we must use intent to start
final Intent intent = new Intent(Settings.ACTION_APN_SETTINGS);
intent.setPackage(mContext.getPackageName());
// This will setup the Home and Search affordance
intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, true);
intent.putExtra(ApnSettings.SUB_ID, mSubId);
mContext.startActivity(intent);
return true;
}
return false;
}
public void init(int subId) {
mSubId = subId;
}
@VisibleForTesting
void setPreference(Preference preference) {
mPreference = preference;
}
private class DpcApnEnforcedObserver extends ContentObserver {
DpcApnEnforcedObserver(Handler handler) {
super(handler);
}
public void register(Context context) {
context.getContentResolver().registerContentObserver(ENFORCE_MANAGED_URI, false, this);
}
public void unRegister(Context context) {
context.getContentResolver().unregisterContentObserver(this);
}
@Override
public void onChange(boolean selfChange) {
updateState(mPreference);
}
}
}
ApnPreferenceControllerTest 源码
packages/apps/Settings/tests/unit/src/com/android/settings/network/telephony/ApnPreferenceControllerTest.java
/*
* Copyright (C) 2020 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.settings.network.telephony;
import static com.android.settings.core.BasePreferenceController.AVAILABLE;
import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.os.PersistableBundle;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.network.CarrierConfigCache;
import com.android.settings.network.apn.ApnSettings;
import com.android.settingslib.RestrictedPreference;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@RunWith(AndroidJUnit4.class)
public class ApnPreferenceControllerTest {
private static final int SUB_ID = 2;
@Mock
private TelephonyManager mTelephonyManager;
@Mock
private TelephonyManager mInvalidTelephonyManager;
@Mock
private SubscriptionManager mSubscriptionManager;
@Mock
private CarrierConfigCache mCarrierConfigCache;
private ApnPreferenceController mController;
private RestrictedPreference mPreference;
private Context mContext;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephonyManager);
when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
CarrierConfigCache.setTestInstance(mContext, mCarrierConfigCache);
doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(SUB_ID);
doReturn(mInvalidTelephonyManager).when(mTelephonyManager).createForSubscriptionId(
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mPreference = new RestrictedPreference(mContext);
mController = new ApnPreferenceController(mContext, "mobile_data");
mController.init(SUB_ID);
mController.setPreference(mPreference);
mPreference.setKey(mController.getPreferenceKey());
}
@Test
public void getAvailabilityStatus_apnSettingsNotSupported_returnUnavailable() {
doReturn(TelephonyManager.PHONE_TYPE_CDMA).when(mTelephonyManager).getPhoneType();
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_SHOW_APN_SETTING_CDMA_BOOL, false);
doReturn(bundle).when(mCarrierConfigCache).getConfigForSubId(SUB_ID);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
public void getAvailabilityStatus_apnSettingsSupportedWithCDMA_returnAvailable() {
doReturn(TelephonyManager.PHONE_TYPE_CDMA).when(mTelephonyManager).getPhoneType();
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_SHOW_APN_SETTING_CDMA_BOOL, true);
doReturn(bundle).when(mCarrierConfigCache).getConfigForSubId(SUB_ID);
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailabilityStatus_apnSettingsSupportedWithGsm_returnAvailable() {
doReturn(TelephonyManager.PHONE_TYPE_GSM).when(mTelephonyManager).getPhoneType();
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_APN_EXPAND_BOOL, true);
doReturn(bundle).when(mCarrierConfigCache).getConfigForSubId(SUB_ID);
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
@Test
public void getAvailabilityStatus_carrierConfigNull_returnUnavailable() {
doReturn(TelephonyManager.PHONE_TYPE_GSM).when(mTelephonyManager).getPhoneType();
when(mCarrierConfigCache.getConfigForSubId(SUB_ID)).thenReturn(null);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
public void getAvailabilityStatus_hideCarrierNetworkSettings_returnUnavailable() {
doReturn(TelephonyManager.PHONE_TYPE_GSM).when(mTelephonyManager).getPhoneType();
final PersistableBundle bundle = new PersistableBundle();
bundle.putBoolean(CarrierConfigManager.KEY_APN_EXPAND_BOOL, true);
bundle.putBoolean(CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL, true);
doReturn(bundle).when(mCarrierConfigCache).getConfigForSubId(SUB_ID);
assertThat(mController.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE);
}
@Test
public void handPreferenceTreeClick_fireIntent() {
ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(captor.capture());
mController.handlePreferenceTreeClick(mPreference);
final Intent intent = captor.getValue();
assertThat(intent.getAction()).isEqualTo(Settings.ACTION_APN_SETTINGS);
assertThat(intent.getIntExtra(ApnSettings.SUB_ID, 0)).isEqualTo(SUB_ID);
}
}
ApnEditor 编辑页
packages/apps/Settings/src/com/android/settings/network/apn/ApnEditor.java
/*
* Copyright (C) 2006 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.settings.network.apn;
import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.UserManager;
import android.provider.Telephony;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnKeyListener;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.Preference;
import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.TwoStatePreference;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.network.ProxySubscriptionManager;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/** Use to edit apn settings. */
public class ApnEditor extends SettingsPreferenceFragment
implements OnPreferenceChangeListener, OnKeyListener {
private static final String TAG = ApnEditor.class.getSimpleName();
private static final boolean VDBG = false; // STOPSHIP if true
private static final String KEY_AUTH_TYPE = "auth_type";
private static final String KEY_APN_TYPE = "apn_type";
private static final String KEY_PROTOCOL = "apn_protocol";
private static final String KEY_ROAMING_PROTOCOL = "apn_roaming_protocol";
private static final String KEY_CARRIER_ENABLED = "carrier_enabled";
private static final String KEY_BEARER_MULTI = "bearer_multi";
private static final String KEY_MVNO_TYPE = "mvno_type";
private static final String KEY_PASSWORD = "apn_password";
@VisibleForTesting
static final int MENU_DELETE = Menu.FIRST;
private static final int MENU_SAVE = Menu.FIRST + 1;
private static final int MENU_CANCEL = Menu.FIRST + 2;
@VisibleForTesting
static String sNotSet;
@VisibleForTesting
EditTextPreference mName;
@VisibleForTesting
EditTextPreference mApn;
@VisibleForTesting
EditTextPreference mProxy;
@VisibleForTesting
EditTextPreference mPort;
@VisibleForTesting
EditTextPreference mUser;
@VisibleForTesting
EditTextPreference mServer;
@VisibleForTesting
EditTextPreference mPassword;
@VisibleForTesting
EditTextPreference mMmsc;
@VisibleForTesting
EditTextPreference mMcc;
@VisibleForTesting
EditTextPreference mMnc;
@VisibleForTesting
EditTextPreference mMmsProxy;
@VisibleForTesting
EditTextPreference mMmsPort;
@VisibleForTesting
ListPreference mAuthType;
@VisibleForTesting
EditTextPreference mApnType;
@VisibleForTesting
ListPreference mProtocol;
@VisibleForTesting
ListPreference mRoamingProtocol;
@VisibleForTesting
TwoStatePreference mCarrierEnabled;
@VisibleForTesting
MultiSelectListPreference mBearerMulti;
@VisibleForTesting
ListPreference mMvnoType;
@VisibleForTesting
EditTextPreference mMvnoMatchData;
@VisibleForTesting
ApnData mApnData;
private String mCurMnc;
private String mCurMcc;
private boolean mNewApn;
private int mSubId;
@VisibleForTesting
ProxySubscriptionManager mProxySubscriptionMgr;
private int mBearerInitialVal = 0;
private String mMvnoTypeStr;
private String mMvnoMatchDataStr;
@VisibleForTesting
String[] mReadOnlyApnTypes;
@VisibleForTesting
String[] mDefaultApnTypes;
@VisibleForTesting
String mDefaultApnProtocol;
@VisibleForTesting
String mDefaultApnRoamingProtocol;
private String[] mReadOnlyApnFields;
private boolean mReadOnlyApn;
/**
* The APN deletion feature within menu is aligned with the APN adding feature.
* Having only one of them could lead to a UX which not that make sense from user's
* perspective.
*
* mIsAddApnAllowed stores the configuration value reading from
* CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL to support the presentation
* control of the menu options. When false, delete option would be invisible to
* the end user.
*/
private boolean mIsAddApnAllowed;
private Uri mCarrierUri;
private boolean mIsCarrierIdApn;
/**
* APN types for data connections. These are usage categories for an APN
* entry. One APN entry may support multiple APN types, eg, a single APN
* may service regular internet traffic ("default") as well as MMS-specific
* connections.<br/>
* APN_TYPE_ALL is a special type to indicate that this APN entry can
* service all data connections.
*/
public static final String APN_TYPE_ALL = "*";
/** APN type for default data traffic */
public static final String APN_TYPE_DEFAULT = "default";
/** APN type for MMS traffic */
public static final String APN_TYPE_MMS = "mms";
/** APN type for SUPL assisted GPS */
public static final String APN_TYPE_SUPL = "supl";
/** APN type for DUN traffic */
public static final String APN_TYPE_DUN = "dun";
/** APN type for HiPri traffic */
public static final String APN_TYPE_HIPRI = "hipri";
/** APN type for FOTA */
public static final String APN_TYPE_FOTA = "fota";
/** APN type for IMS */
public static final String APN_TYPE_IMS = "ims";
/** APN type for CBS */
public static final String APN_TYPE_CBS = "cbs";
/** APN type for IA Initial Attach APN */
public static final String APN_TYPE_IA = "ia";
/** APN type for Emergency PDN. This is not an IA apn, but is used
* for access to carrier services in an emergency call situation. */
public static final String APN_TYPE_EMERGENCY = "emergency";
/** APN type for Mission Critical Services */
public static final String APN_TYPE_MCX = "mcx";
/** APN type for XCAP */
public static final String APN_TYPE_XCAP = "xcap";
/** Array of all APN types */
public static final String[] APN_TYPES = {APN_TYPE_DEFAULT,
APN_TYPE_MMS,
APN_TYPE_SUPL,
APN_TYPE_DUN,
APN_TYPE_HIPRI,
APN_TYPE_FOTA,
APN_TYPE_IMS,
APN_TYPE_CBS,
APN_TYPE_IA,
APN_TYPE_EMERGENCY,
APN_TYPE_MCX,
APN_TYPE_XCAP,
};
/**
* Standard projection for the interesting columns of a normal note.
*/
private static final String[] sProjection = new String[] {
Telephony.Carriers._ID, // 0
Telephony.Carriers.NAME, // 1
Telephony.Carriers.APN, // 2
Telephony.Carriers.PROXY, // 3
Telephony.Carriers.PORT, // 4
Telephony.Carriers.USER, // 5
Telephony.Carriers.SERVER, // 6
Telephony.Carriers.PASSWORD, // 7
Telephony.Carriers.MMSC, // 8
Telephony.Carriers.MCC, // 9
Telephony.Carriers.MNC, // 10
Telephony.Carriers.NUMERIC, // 11
Telephony.Carriers.MMSPROXY, // 12
Telephony.Carriers.MMSPORT, // 13
Telephony.Carriers.AUTH_TYPE, // 14
Telephony.Carriers.TYPE, // 15
Telephony.Carriers.PROTOCOL, // 16
Telephony.Carriers.CARRIER_ENABLED, // 17
Telephony.Carriers.BEARER, // 18
Telephony.Carriers.BEARER_BITMASK, // 19
Telephony.Carriers.ROAMING_PROTOCOL, // 20
Telephony.Carriers.MVNO_TYPE, // 21
Telephony.Carriers.MVNO_MATCH_DATA, // 22
Telephony.Carriers.EDITED_STATUS, // 23
Telephony.Carriers.USER_EDITABLE, // 24
Telephony.Carriers.CARRIER_ID // 25
};
private static final int ID_INDEX = 0;
@VisibleForTesting
static final int NAME_INDEX = 1;
@VisibleForTesting
static final int APN_INDEX = 2;
private static final int PROXY_INDEX = 3;
private static final int PORT_INDEX = 4;
private static final int USER_INDEX = 5;
private static final int SERVER_INDEX = 6;
private static final int PASSWORD_INDEX = 7;
private static final int MMSC_INDEX = 8;
@VisibleForTesting
static final int MCC_INDEX = 9;
@VisibleForTesting
static final int MNC_INDEX = 10;
private static final int MMSPROXY_INDEX = 12;
private static final int MMSPORT_INDEX = 13;
private static final int AUTH_TYPE_INDEX = 14;
@VisibleForTesting
static final int TYPE_INDEX = 15;
@VisibleForTesting
static final int PROTOCOL_INDEX = 16;
@VisibleForTesting
static final int CARRIER_ENABLED_INDEX = 17;
private static final int BEARER_INDEX = 18;
private static final int BEARER_BITMASK_INDEX = 19;
@VisibleForTesting
static final int ROAMING_PROTOCOL_INDEX = 20;
private static final int MVNO_TYPE_INDEX = 21;
private static final int MVNO_MATCH_DATA_INDEX = 22;
private static final int EDITED_INDEX = 23;
private static final int USER_EDITABLE_INDEX = 24;
private static final int CARRIER_ID_INDEX = 25;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
if (isUserRestricted()) {
Log.e(TAG, "This setting isn't available due to user restriction.");
finish();
return;
}
setLifecycleForAllControllers();
final Intent intent = getIntent();
final String action = intent.getAction();
if (TextUtils.isEmpty(action)) {
finish();
return;
}
mSubId = intent.getIntExtra(ApnSettings.SUB_ID,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
initApnEditorUi();
getCarrierCustomizedConfig(getContext());
Uri uri = null;
if (action.equals(Intent.ACTION_EDIT)) {
uri = intent.getData();
if (!uri.isPathPrefixMatch(Telephony.Carriers.CONTENT_URI)) {
Log.e(TAG, "Edit request not for carrier table. Uri: " + uri);
finish();
return;
}
} else if (action.equals(Intent.ACTION_INSERT)) {
mCarrierUri = intent.getData();
if (!mCarrierUri.isPathPrefixMatch(Telephony.Carriers.CONTENT_URI)) {
Log.e(TAG, "Insert request not for carrier table. Uri: " + mCarrierUri);
finish();
return;
}
mNewApn = true;
mMvnoTypeStr = intent.getStringExtra(ApnSettings.MVNO_TYPE);
mMvnoMatchDataStr = intent.getStringExtra(ApnSettings.MVNO_MATCH_DATA);
} else {
finish();
return;
}
// Creates an ApnData to store the apn data temporary, so that we don't need the cursor to
// get the apn data. The uri is null if the action is ACTION_INSERT, that mean there is no
// record in the database, so create a empty ApnData to represent a empty row of database.
if (uri != null) {
mApnData = getApnDataFromUri(uri);
} else {
mApnData = new ApnData(sProjection.length);
}
final int carrierId = mApnData.getInteger(CARRIER_ID_INDEX,
TelephonyManager.UNKNOWN_CARRIER_ID);
mIsCarrierIdApn = (carrierId > TelephonyManager.UNKNOWN_CARRIER_ID);
final boolean isUserEdited = mApnData.getInteger(EDITED_INDEX,
Telephony.Carriers.USER_EDITED) == Telephony.Carriers.USER_EDITED;
Log.d(TAG, "onCreate: EDITED " + isUserEdited);
// if it's not a USER_EDITED apn, check if it's read-only
if (!isUserEdited && (mApnData.getInteger(USER_EDITABLE_INDEX, 1) == 0
|| apnTypesMatch(mReadOnlyApnTypes, mApnData.getString(TYPE_INDEX)))) {
Log.d(TAG, "onCreate: apnTypesMatch; read-only APN");
mReadOnlyApn = true;
disableAllFields();
} else if (!ArrayUtils.isEmpty(mReadOnlyApnFields)) {
disableFields(mReadOnlyApnFields);
}
// Make sure that a user cannot break carrier id APN matching
if (mIsCarrierIdApn) {
disableFieldsForCarrieridApn();
}
for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
getPreferenceScreen().getPreference(i).setOnPreferenceChangeListener(this);
}
}
/**
* Enable ProxySubscriptionMgr with Lifecycle support for all controllers
* live within this fragment
*/
private void setLifecycleForAllControllers() {
if (mProxySubscriptionMgr == null) {
mProxySubscriptionMgr = ProxySubscriptionManager.getInstance(getContext());
}
mProxySubscriptionMgr.setLifecycle(getLifecycle());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
fillUI(savedInstanceState == null);
setCarrierCustomizedConfigToUi();
}
@VisibleForTesting
static String formatInteger(String value) {
try {
final int intValue = Integer.parseInt(value);
return String.format(getCorrectDigitsFormat(value), intValue);
} catch (NumberFormatException e) {
return value;
}
}
/**
* Get the digits format so we preserve leading 0's.
* MCCs are 3 digits and MNCs are either 2 or 3.
*/
static String getCorrectDigitsFormat(String value) {
if (value.length() == 2) return "%02d";
else return "%03d";
}
/**
* Check if passed in array of APN types indicates all APN types
* @param apnTypes array of APN types. "*" indicates all types.
* @return true if all apn types are included in the array, false otherwise
*/
static boolean hasAllApns(String[] apnTypes) {
if (ArrayUtils.isEmpty(apnTypes)) {
return false;
}
final List apnList = Arrays.asList(apnTypes);
if (apnList.contains(APN_TYPE_ALL)) {
Log.d(TAG, "hasAllApns: true because apnList.contains(APN_TYPE_ALL)");
return true;
}
for (String apn : APN_TYPES) {
if (!apnList.contains(apn)) {
return false;
}
}
Log.d(TAG, "hasAllApns: true");
return true;
}
/**
* Check if APN types overlap.
* @param apnTypesArray1 array of APNs. Empty array indicates no APN type; "*" indicates all
* types
* @param apnTypes2 comma separated string of APN types. Empty string represents all types.
* @return if any apn type matches return true, otherwise return false
*/
private boolean apnTypesMatch(String[] apnTypesArray1, String apnTypes2) {
if (ArrayUtils.isEmpty(apnTypesArray1)) {
return false;
}
final String[] apnTypesArray1LowerCase = new String[apnTypesArray1.length];
for (int i = 0; i < apnTypesArray1.length; i++) {
apnTypesArray1LowerCase[i] = apnTypesArray1[i].toLowerCase();
}
if (hasAllApns(apnTypesArray1LowerCase) || TextUtils.isEmpty(apnTypes2)) {
return true;
}
final List apnTypesList1 = Arrays.asList(apnTypesArray1LowerCase);
final String[] apnTypesArray2 = apnTypes2.split(",");
for (String apn : apnTypesArray2) {
if (apnTypesList1.contains(apn.trim().toLowerCase())) {
Log.d(TAG, "apnTypesMatch: true because match found for " + apn.trim());
return true;
}
}
Log.d(TAG, "apnTypesMatch: false");
return false;
}
/**
* Function to get Preference obj corresponding to an apnField
* @param apnField apn field name for which pref is needed
* @return Preference obj corresponding to passed in apnField
*/
private Preference getPreferenceFromFieldName(String apnField) {
switch (apnField) {
case Telephony.Carriers.NAME:
return mName;
case Telephony.Carriers.APN:
return mApn;
case Telephony.Carriers.PROXY:
return mProxy;
case Telephony.Carriers.PORT:
return mPort;
case Telephony.Carriers.USER:
return mUser;
case Telephony.Carriers.SERVER:
return mServer;
case Telephony.Carriers.PASSWORD:
return mPassword;
case Telephony.Carriers.MMSPROXY:
return mMmsProxy;
case Telephony.Carriers.MMSPORT:
return mMmsPort;
case Telephony.Carriers.MMSC:
return mMmsc;
case Telephony.Carriers.MCC:
return mMcc;
case Telephony.Carriers.MNC:
return mMnc;
case Telephony.Carriers.TYPE:
return mApnType;
case Telephony.Carriers.AUTH_TYPE:
return mAuthType;
case Telephony.Carriers.PROTOCOL:
return mProtocol;
case Telephony.Carriers.ROAMING_PROTOCOL:
return mRoamingProtocol;
case Telephony.Carriers.CARRIER_ENABLED:
return mCarrierEnabled;
case Telephony.Carriers.BEARER:
case Telephony.Carriers.BEARER_BITMASK:
return mBearerMulti;
case Telephony.Carriers.MVNO_TYPE:
return mMvnoType;
case Telephony.Carriers.MVNO_MATCH_DATA:
return mMvnoMatchData;
}
return null;
}
/**
* Disables given fields so that user cannot modify them
*
* @param apnFields fields to be disabled
*/
private void disableFields(String[] apnFields) {
for (String apnField : apnFields) {
final Preference preference = getPreferenceFromFieldName(apnField);
if (preference != null) {
preference.setEnabled(false);
}
}
}
/**
* Disables all fields so that user cannot modify the APN
*/
private void disableAllFields() {
mName.setEnabled(false);
mApn.setEnabled(false);
mProxy.setEnabled(false);
mPort.setEnabled(false);
mUser.setEnabled(false);
mServer.setEnabled(false);
mPassword.setEnabled(false);
mMmsProxy.setEnabled(false);
mMmsPort.setEnabled(false);
mMmsc.setEnabled(false);
mMcc.setEnabled(false);
mMnc.setEnabled(false);
mApnType.setEnabled(false);
mAuthType.setEnabled(false);
mProtocol.setEnabled(false);
mRoamingProtocol.setEnabled(false);
mCarrierEnabled.setEnabled(false);
mBearerMulti.setEnabled(false);
mMvnoType.setEnabled(false);
mMvnoMatchData.setEnabled(false);
}
/**
* Disables fields for a carrier id APN to avoid breaking the match criteria
*/
private void disableFieldsForCarrieridApn() {
mMcc.setEnabled(false);
mMnc.setEnabled(false);
mMvnoType.setEnabled(false);
mMvnoMatchData.setEnabled(false);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.APN_EDITOR;
}
@VisibleForTesting
void fillUI(boolean firstTime) {
if (firstTime) {
// Fill in all the values from the db in both text editor and summary
mName.setText(mApnData.getString(NAME_INDEX));
mApn.setText(mApnData.getString(APN_INDEX));
mProxy.setText(mApnData.getString(PROXY_INDEX));
mPort.setText(mApnData.getString(PORT_INDEX));
mUser.setText(mApnData.getString(USER_INDEX));
mServer.setText(mApnData.getString(SERVER_INDEX));
mPassword.setText(mApnData.getString(PASSWORD_INDEX));
mMmsProxy.setText(mApnData.getString(MMSPROXY_INDEX));
mMmsPort.setText(mApnData.getString(MMSPORT_INDEX));
mMmsc.setText(mApnData.getString(MMSC_INDEX));
mMcc.setText(mApnData.getString(MCC_INDEX));
mMnc.setText(mApnData.getString(MNC_INDEX));
mApnType.setText(mApnData.getString(TYPE_INDEX));
if (mNewApn) {
final SubscriptionInfo subInfo =
mProxySubscriptionMgr.getAccessibleSubscriptionInfo(mSubId);
// Country code
final String mcc = (subInfo == null) ? null : subInfo.getMccString();
// Network code
final String mnc = (subInfo == null) ? null : subInfo.getMncString();
if (!TextUtils.isEmpty(mcc)) {
// Auto populate MNC and MCC for new entries, based on what SIM reports
mMcc.setText(mcc);
mMnc.setText(mnc);
mCurMnc = mnc;
mCurMcc = mcc;
}
}
final int authVal = mApnData.getInteger(AUTH_TYPE_INDEX, -1);
if (authVal != -1) {
mAuthType.setValueIndex(authVal);
} else {
mAuthType.setValue(null);
}
mProtocol.setValue(mApnData.getString(PROTOCOL_INDEX));
mRoamingProtocol.setValue(mApnData.getString(ROAMING_PROTOCOL_INDEX));
mCarrierEnabled.setChecked(mApnData.getInteger(CARRIER_ENABLED_INDEX, 1) == 1);
mBearerInitialVal = mApnData.getInteger(BEARER_INDEX, 0);
final HashSet<String> bearers = new HashSet<String>();
int bearerBitmask = mApnData.getInteger(BEARER_BITMASK_INDEX, 0);
if (bearerBitmask == 0) {
if (mBearerInitialVal == 0) {
bearers.add("" + 0);
}
} else {
int i = 1;
while (bearerBitmask != 0) {
if ((bearerBitmask & 1) == 1) {
bearers.add("" + i);
}
bearerBitmask >>= 1;
i++;
}
}
if (mBearerInitialVal != 0 && !bearers.contains("" + mBearerInitialVal)) {
// add mBearerInitialVal to bearers
bearers.add("" + mBearerInitialVal);
}
mBearerMulti.setValues(bearers);
mMvnoType.setValue(mApnData.getString(MVNO_TYPE_INDEX));
mMvnoMatchData.setEnabled(false);
mMvnoMatchData.setText(mApnData.getString(MVNO_MATCH_DATA_INDEX));
if (mNewApn && mMvnoTypeStr != null && mMvnoMatchDataStr != null) {
mMvnoType.setValue(mMvnoTypeStr);
mMvnoMatchData.setText(mMvnoMatchDataStr);
}
}
mName.setSummary(checkNull(mName.getText()));
mApn.setSummary(checkNull(mApn.getText()));
mProxy.setSummary(checkNull(mProxy.getText()));
mPort.setSummary(checkNull(mPort.getText()));
mUser.setSummary(checkNull(mUser.getText()));
mServer.setSummary(checkNull(mServer.getText()));
mPassword.setSummary(starify(mPassword.getText()));
mMmsProxy.setSummary(checkNull(mMmsProxy.getText()));
mMmsPort.setSummary(checkNull(mMmsPort.getText()));
mMmsc.setSummary(checkNull(mMmsc.getText()));
mMcc.setSummary(formatInteger(checkNull(mMcc.getText())));
mMnc.setSummary(formatInteger(checkNull(mMnc.getText())));
mApnType.setSummary(checkNull(mApnType.getText()));
final String authVal = mAuthType.getValue();
if (authVal != null) {
final int authValIndex = Integer.parseInt(authVal);
mAuthType.setValueIndex(authValIndex);
final String[] values = getResources().getStringArray(R.array.apn_auth_entries);
mAuthType.setSummary(values[authValIndex]);
} else {
mAuthType.setSummary(sNotSet);
}
mProtocol.setSummary(checkNull(protocolDescription(mProtocol.getValue(), mProtocol)));
mRoamingProtocol.setSummary(
checkNull(protocolDescription(mRoamingProtocol.getValue(), mRoamingProtocol)));
mBearerMulti.setSummary(
checkNull(bearerMultiDescription(mBearerMulti.getValues())));
mMvnoType.setSummary(
checkNull(mvnoDescription(mMvnoType.getValue())));
mMvnoMatchData.setSummary(checkNullforMvnoValue(mMvnoMatchData.getText()));
// allow user to edit carrier_enabled for some APN
final boolean ceEditable = getResources().getBoolean(
R.bool.config_allow_edit_carrier_enabled);
if (ceEditable) {
mCarrierEnabled.setEnabled(true);
} else {
mCarrierEnabled.setEnabled(false);
}
}
/**
* Returns the UI choice (e.g., "IPv4/IPv6") corresponding to the given
* raw value of the protocol preference (e.g., "IPV4V6"). If unknown,
* return null.
*/
private String protocolDescription(String raw, ListPreference protocol) {
String uRaw = checkNull(raw).toUpperCase();
uRaw = uRaw.equals("IPV4") ? "IP" : uRaw;
final int protocolIndex = protocol.findIndexOfValue(uRaw);
if (protocolIndex == -1) {
return null;
} else {
final String[] values = getResources().getStringArray(R.array.apn_protocol_entries);
try {
return values[protocolIndex];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
}
}
private String bearerMultiDescription(Set<String> raw) {
final String[] values = getResources().getStringArray(R.array.bearer_entries);
final StringBuilder retVal = new StringBuilder();
boolean first = true;
for (String bearer : raw) {
int bearerIndex = mBearerMulti.findIndexOfValue(bearer);
try {
if (first) {
retVal.append(values[bearerIndex]);
first = false;
} else {
retVal.append(", " + values[bearerIndex]);
}
} catch (ArrayIndexOutOfBoundsException e) {
// ignore
}
}
final String val = retVal.toString();
if (!TextUtils.isEmpty(val)) {
return val;
}
return null;
}
private String mvnoDescription(String newValue) {
final int mvnoIndex = mMvnoType.findIndexOfValue(newValue);
final String oldValue = mMvnoType.getValue();
if (mvnoIndex == -1) {
return null;
} else {
final String[] values = getResources().getStringArray(R.array.mvno_type_entries);
final boolean mvnoMatchDataUneditable =
mReadOnlyApn || (mReadOnlyApnFields != null
&& Arrays.asList(mReadOnlyApnFields)
.contains(Telephony.Carriers.MVNO_MATCH_DATA));
mMvnoMatchData.setEnabled(!mvnoMatchDataUneditable && mvnoIndex != 0);
if (newValue != null && !newValue.equals(oldValue)) {
if (values[mvnoIndex].equals("SPN")) {
TelephonyManager telephonyManager = (TelephonyManager)
getContext().getSystemService(TelephonyManager.class);
final TelephonyManager telephonyManagerForSubId =
telephonyManager.createForSubscriptionId(mSubId);
if (telephonyManagerForSubId != null) {
telephonyManager = telephonyManagerForSubId;
}
mMvnoMatchData.setText(telephonyManager.getSimOperatorName());
} else if (values[mvnoIndex].equals("IMSI")) {
final SubscriptionInfo subInfo =
mProxySubscriptionMgr.getAccessibleSubscriptionInfo(mSubId);
final String mcc = (subInfo == null) ? "" :
Objects.toString(subInfo.getMccString(), "");
final String mnc = (subInfo == null) ? "" :
Objects.toString(subInfo.getMncString(), "");
mMvnoMatchData.setText(mcc + mnc + "x");
} else if (values[mvnoIndex].equals("GID")) {
TelephonyManager telephonyManager = (TelephonyManager)
getContext().getSystemService(TelephonyManager.class);
final TelephonyManager telephonyManagerForSubId =
telephonyManager.createForSubscriptionId(mSubId);
if (telephonyManagerForSubId != null) {
telephonyManager = telephonyManagerForSubId;
}
mMvnoMatchData.setText(telephonyManager.getGroupIdLevel1());
} else {
// mvno type 'none' case. At this time, mvnoIndex should be 0.
mMvnoMatchData.setText("");
}
}
try {
return values[mvnoIndex];
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
}
}
/**
* Callback when preference status changed.
*/
public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey();
if (KEY_AUTH_TYPE.equals(key)) {
try {
final int index = Integer.parseInt((String) newValue);
mAuthType.setValueIndex(index);
final String[] values = getResources().getStringArray(R.array.apn_auth_entries);
mAuthType.setSummary(values[index]);
} catch (NumberFormatException e) {
return false;
}
} else if (KEY_APN_TYPE.equals(key)) {
String data = (TextUtils.isEmpty((String) newValue)
&& !ArrayUtils.isEmpty(mDefaultApnTypes))
? getEditableApnType(mDefaultApnTypes) : (String) newValue;
if (!TextUtils.isEmpty(data)) {
mApnType.setSummary(data);
}
} else if (KEY_PROTOCOL.equals(key)) {
final String protocol = protocolDescription((String) newValue, mProtocol);
if (protocol == null) {
return false;
}
mProtocol.setSummary(protocol);
mProtocol.setValue((String) newValue);
} else if (KEY_ROAMING_PROTOCOL.equals(key)) {
final String protocol = protocolDescription((String) newValue, mRoamingProtocol);
if (protocol == null) {
return false;
}
mRoamingProtocol.setSummary(protocol);
mRoamingProtocol.setValue((String) newValue);
} else if (KEY_BEARER_MULTI.equals(key)) {
final String bearer = bearerMultiDescription((Set<String>) newValue);
if (bearer == null) {
return false;
}
mBearerMulti.setValues((Set<String>) newValue);
mBearerMulti.setSummary(bearer);
} else if (KEY_MVNO_TYPE.equals(key)) {
final String mvno = mvnoDescription((String) newValue);
if (mvno == null) {
return false;
}
mMvnoType.setValue((String) newValue);
mMvnoType.setSummary(mvno);
mMvnoMatchData.setSummary(checkNullforMvnoValue(mMvnoMatchData.getText()));
} else if (KEY_PASSWORD.equals(key)) {
mPassword.setSummary(starify(newValue != null ? String.valueOf(newValue) : ""));
} else if (KEY_CARRIER_ENABLED.equals(key)) {
// do nothing
} else {
preference.setSummary(checkNull(newValue != null ? String.valueOf(newValue) : null));
}
return true;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// If it's a new APN, then cancel will delete the new entry in onPause
// If APN add is not allowed, delete might lead to issue regarding recovery
if (!mNewApn && !mReadOnlyApn && mIsAddApnAllowed) {
menu.add(0, MENU_DELETE, 0, R.string.menu_delete)
.setIcon(R.drawable.ic_delete);
}
if (!mReadOnlyApn) {
menu.add(0, MENU_SAVE, 0, R.string.menu_save)
.setIcon(android.R.drawable.ic_menu_save);
}
menu.add(0, MENU_CANCEL, 0, R.string.menu_cancel)
.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_DELETE:
deleteApn();
finish();
return true;
case MENU_SAVE:
if (validateAndSaveApnData()) {
finish();
}
return true;
case MENU_CANCEL:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.setOnKeyListener(this);
view.setFocusableInTouchMode(true);
view.requestFocus();
}
/**
* Try to save the apn data when pressed the back button. An error message will be displayed if
* the apn data is invalid.
*
* TODO(b/77339593): Try to keep the same behavior between back button and up navigate button.
* We will save the valid apn data to the database when pressed the back button, but discard all
* user changed when pressed the up navigate button.
*/
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) return false;
switch (keyCode) {
case KeyEvent.KEYCODE_BACK: {
if (validateAndSaveApnData()) {
finish();
}
return true;
}
}
return false;
}
/**
* Add key, value to {@code cv} and compare the value against the value at index in
* {@link #mApnData}.
*
* <p>
* The key, value will not add to {@code cv} if value is null.
*
* @return true if values are different. {@code assumeDiff} indicates if values can be assumed
* different in which case no comparison is needed.
*/
boolean setStringValueAndCheckIfDiff(
ContentValues cv, String key, String value, boolean assumeDiff, int index) {
final String valueFromLocalCache = mApnData.getString(index);
if (VDBG) {
Log.d(TAG, "setStringValueAndCheckIfDiff: assumeDiff: " + assumeDiff
+ " key: " + key
+ " value: '" + value
+ "' valueFromDb: '" + valueFromLocalCache + "'");
}
final boolean isDiff = assumeDiff
|| !((TextUtils.isEmpty(value) && TextUtils.isEmpty(valueFromLocalCache))
|| (value != null && value.equals(valueFromLocalCache)));
if (isDiff && value != null) {
cv.put(key, value);
}
return isDiff;
}
/**
* Add key, value to {@code cv} and compare the value against the value at index in
* {@link #mApnData}.
*
* @return true if values are different. {@code assumeDiff} indicates if values can be assumed
* different in which case no comparison is needed.
*/
boolean setIntValueAndCheckIfDiff(
ContentValues cv, String key, int value, boolean assumeDiff, int index) {
final Integer valueFromLocalCache = mApnData.getInteger(index);
if (VDBG) {
Log.d(TAG, "setIntValueAndCheckIfDiff: assumeDiff: " + assumeDiff
+ " key: " + key
+ " value: '" + value
+ "' valueFromDb: '" + valueFromLocalCache + "'");
}
final boolean isDiff = assumeDiff || value != valueFromLocalCache;
if (isDiff) {
cv.put(key, value);
}
return isDiff;
}
/**
* Validates the apn data and save it to the database if it's valid.
*
* <p>
* A dialog with error message will be displayed if the APN data is invalid.
*
* @return true if there is no error
*/
@VisibleForTesting
boolean validateAndSaveApnData() {
// Nothing to do if it's a read only APN
if (mReadOnlyApn) {
return true;
}
final String name = checkNotSet(mName.getText());
final String apn = checkNotSet(mApn.getText());
final String mcc = checkNotSet(mMcc.getText());
final String mnc = checkNotSet(mMnc.getText());
final String errorMsg = validateApnData();
if (errorMsg != null) {
showError();
return false;
}
final ContentValues values = new ContentValues();
// call update() if it's a new APN. If not, check if any field differs from the db value;
// if any diff is found update() should be called
boolean callUpdate = mNewApn;
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.NAME,
name,
callUpdate,
NAME_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.APN,
apn,
callUpdate,
APN_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.PROXY,
checkNotSet(mProxy.getText()),
callUpdate,
PROXY_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.PORT,
checkNotSet(mPort.getText()),
callUpdate,
PORT_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MMSPROXY,
checkNotSet(mMmsProxy.getText()),
callUpdate,
MMSPROXY_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MMSPORT,
checkNotSet(mMmsPort.getText()),
callUpdate,
MMSPORT_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.USER,
checkNotSet(mUser.getText()),
callUpdate,
USER_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.SERVER,
checkNotSet(mServer.getText()),
callUpdate,
SERVER_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.PASSWORD,
checkNotSet(mPassword.getText()),
callUpdate,
PASSWORD_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MMSC,
checkNotSet(mMmsc.getText()),
callUpdate,
MMSC_INDEX);
final String authVal = mAuthType.getValue();
if (authVal != null) {
callUpdate = setIntValueAndCheckIfDiff(values,
Telephony.Carriers.AUTH_TYPE,
Integer.parseInt(authVal),
callUpdate,
AUTH_TYPE_INDEX);
}
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.PROTOCOL,
checkNotSet(mProtocol.getValue()),
callUpdate,
PROTOCOL_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.ROAMING_PROTOCOL,
checkNotSet(mRoamingProtocol.getValue()),
callUpdate,
ROAMING_PROTOCOL_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.TYPE,
checkNotSet(getUserEnteredApnType()),
callUpdate,
TYPE_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MCC,
mcc,
callUpdate,
MCC_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MNC,
mnc,
callUpdate,
MNC_INDEX);
values.put(Telephony.Carriers.NUMERIC, mcc + mnc);
if (mCurMnc != null && mCurMcc != null) {
if (mCurMnc.equals(mnc) && mCurMcc.equals(mcc)) {
values.put(Telephony.Carriers.CURRENT, 1);
}
}
final Set<String> bearerSet = mBearerMulti.getValues();
int bearerBitmask = 0;
for (String bearer : bearerSet) {
if (Integer.parseInt(bearer) == 0) {
bearerBitmask = 0;
break;
} else {
bearerBitmask |= getBitmaskForTech(Integer.parseInt(bearer));
}
}
callUpdate = setIntValueAndCheckIfDiff(values,
Telephony.Carriers.BEARER_BITMASK,
bearerBitmask,
callUpdate,
BEARER_BITMASK_INDEX);
int bearerVal;
if (bearerBitmask == 0 || mBearerInitialVal == 0) {
bearerVal = 0;
} else if (bitmaskHasTech(bearerBitmask, mBearerInitialVal)) {
bearerVal = mBearerInitialVal;
} else {
// bearer field was being used but bitmask has changed now and does not include the
// initial bearer value -- setting bearer to 0 but maybe better behavior is to choose a
// random tech from the new bitmask??
bearerVal = 0;
}
callUpdate = setIntValueAndCheckIfDiff(values,
Telephony.Carriers.BEARER,
bearerVal,
callUpdate,
BEARER_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MVNO_TYPE,
checkNotSet(mMvnoType.getValue()),
callUpdate,
MVNO_TYPE_INDEX);
callUpdate = setStringValueAndCheckIfDiff(values,
Telephony.Carriers.MVNO_MATCH_DATA,
checkNotSet(mMvnoMatchData.getText()),
callUpdate,
MVNO_MATCH_DATA_INDEX);
callUpdate = setIntValueAndCheckIfDiff(values,
Telephony.Carriers.CARRIER_ENABLED,
mCarrierEnabled.isChecked() ? 1 : 0,
callUpdate,
CARRIER_ENABLED_INDEX);
values.put(Telephony.Carriers.EDITED_STATUS, Telephony.Carriers.USER_EDITED);
if (callUpdate) {
final Uri uri = mApnData.getUri() == null ? mCarrierUri : mApnData.getUri();
updateApnDataToDatabase(uri, values);
} else {
if (VDBG) Log.d(TAG, "validateAndSaveApnData: not calling update()");
}
return true;
}
private void updateApnDataToDatabase(Uri uri, ContentValues values) {
ThreadUtils.postOnBackgroundThread(() -> {
if (uri.equals(mCarrierUri)) {
// Add a new apn to the database
final Uri newUri = getContentResolver().insert(mCarrierUri, values);
if (newUri == null) {
Log.e(TAG, "Can't add a new apn to database " + mCarrierUri);
}
} else {
// Update the existing apn
getContentResolver().update(
uri, values, null /* where */, null /* selection Args */);
}
});
}
/**
* Validates whether the apn data is valid.
*
* @return An error message if the apn data is invalid, otherwise return null.
*/
@VisibleForTesting
String validateApnData() {
String errorMsg = null;
final String name = checkNotSet(mName.getText());
final String apn = checkNotSet(mApn.getText());
final String mcc = checkNotSet(mMcc.getText());
final String mnc = checkNotSet(mMnc.getText());
boolean doNotCheckMccMnc = mIsCarrierIdApn && TextUtils.isEmpty(mcc)
&& TextUtils.isEmpty(mnc);
if (TextUtils.isEmpty(name)) {
errorMsg = getResources().getString(R.string.error_name_empty);
} else if (TextUtils.isEmpty(apn)) {
errorMsg = getResources().getString(R.string.error_apn_empty);
} else if (doNotCheckMccMnc) {
Log.d(TAG, "validateApnData: carrier id APN does not have mcc/mnc defined");
// no op, skip mcc mnc null check
} else if (mcc == null || mcc.length() != 3) {
errorMsg = getResources().getString(R.string.error_mcc_not3);
} else if ((mnc == null || (mnc.length() & 0xFFFE) != 2)) {
errorMsg = getResources().getString(R.string.error_mnc_not23);
}
if (errorMsg == null) {
// if carrier does not allow editing certain apn types, make sure type does not include
// those
if (!ArrayUtils.isEmpty(mReadOnlyApnTypes)
&& apnTypesMatch(mReadOnlyApnTypes, getUserEnteredApnType())) {
final StringBuilder stringBuilder = new StringBuilder();
for (String type : mReadOnlyApnTypes) {
stringBuilder.append(type).append(", ");
Log.d(TAG, "validateApnData: appending type: " + type);
}
// remove last ", "
if (stringBuilder.length() >= 2) {
stringBuilder.delete(stringBuilder.length() - 2, stringBuilder.length());
}
errorMsg = String.format(getResources().getString(R.string.error_adding_apn_type),
stringBuilder);
}
}
return errorMsg;
}
@VisibleForTesting
void showError() {
ErrorDialog.showError(this);
}
private void deleteApn() {
if (mApnData.getUri() != null) {
getContentResolver().delete(mApnData.getUri(), null, null);
mApnData = new ApnData(sProjection.length);
}
}
private String starify(String value) {
if (value == null || value.length() == 0) {
return sNotSet;
} else {
final char[] password = new char[value.length()];
for (int i = 0; i < password.length; i++) {
password[i] = '*';
}
return new String(password);
}
}
/**
* Returns {@link #sNotSet} if the given string {@code value} is null or empty. The string
* {@link #sNotSet} typically used as the default display when an entry in the preference is
* null or empty.
*/
private String checkNull(String value) {
return TextUtils.isEmpty(value) ? sNotSet : value;
}
/**
* To make traslation be diversity, use another string id for MVNO value.
*/
private String checkNullforMvnoValue(String value) {
String notSetForMvnoValue = getResources().getString(R.string.apn_not_set_for_mvno);
return TextUtils.isEmpty(value) ? notSetForMvnoValue : value;
}
/**
* Returns null if the given string {@code value} equals to {@link #sNotSet}. This method
* should be used when convert a string value from preference to database.
*/
private String checkNotSet(String value) {
return sNotSet.equals(value) ? null : value;
}
@VisibleForTesting
String getUserEnteredApnType() {
// if user has not specified a type, map it to "ALL APN TYPES THAT ARE NOT READ-ONLY"
// but if user enter empty type, map it just for default
String userEnteredApnType = mApnType.getText();
if (userEnteredApnType != null) userEnteredApnType = userEnteredApnType.trim();
if ((TextUtils.isEmpty(userEnteredApnType)
|| APN_TYPE_ALL.equals(userEnteredApnType))) {
userEnteredApnType = getEditableApnType(APN_TYPES);
}
Log.d(TAG, "getUserEnteredApnType: changed apn type to editable apn types: "
+ userEnteredApnType);
return userEnteredApnType;
}
private String getEditableApnType(String[] apnTypeList) {
final StringBuilder editableApnTypes = new StringBuilder();
final List<String> readOnlyApnTypes = Arrays.asList(mReadOnlyApnTypes);
boolean first = true;
for (String apnType : apnTypeList) {
// add APN type if it is not read-only and is not wild-cardable
if (!readOnlyApnTypes.contains(apnType)
&& !apnType.equals(APN_TYPE_IA)
&& !apnType.equals(APN_TYPE_EMERGENCY)
&& !apnType.equals(APN_TYPE_MCX)
&& !apnType.equals(APN_TYPE_IMS)) {
if (first) {
first = false;
} else {
editableApnTypes.append(",");
}
editableApnTypes.append(apnType);
}
}
return editableApnTypes.toString();
}
private void initApnEditorUi() {
addPreferencesFromResource(R.xml.apn_editor);
sNotSet = getResources().getString(R.string.apn_not_set);
mName = (EditTextPreference) findPreference("apn_name");
mApn = (EditTextPreference) findPreference("apn_apn");
mProxy = (EditTextPreference) findPreference("apn_http_proxy");
mPort = (EditTextPreference) findPreference("apn_http_port");
mUser = (EditTextPreference) findPreference("apn_user");
mServer = (EditTextPreference) findPreference("apn_server");
mPassword = (EditTextPreference) findPreference(KEY_PASSWORD);
mMmsProxy = (EditTextPreference) findPreference("apn_mms_proxy");
mMmsPort = (EditTextPreference) findPreference("apn_mms_port");
mMmsc = (EditTextPreference) findPreference("apn_mmsc");
mMcc = (EditTextPreference) findPreference("apn_mcc");
mMnc = (EditTextPreference) findPreference("apn_mnc");
mApnType = (EditTextPreference) findPreference("apn_type");
mAuthType = (ListPreference) findPreference(KEY_AUTH_TYPE);
mProtocol = (ListPreference) findPreference(KEY_PROTOCOL);
mRoamingProtocol = (ListPreference) findPreference(KEY_ROAMING_PROTOCOL);
mCarrierEnabled = (TwoStatePreference) findPreference(KEY_CARRIER_ENABLED);
mBearerMulti = (MultiSelectListPreference) findPreference(KEY_BEARER_MULTI);
mMvnoType = (ListPreference) findPreference(KEY_MVNO_TYPE);
mMvnoMatchData = (EditTextPreference) findPreference("mvno_match_data");
}
@VisibleForTesting
protected void getCarrierCustomizedConfig(Context context) {
mReadOnlyApn = false;
mReadOnlyApnTypes = null;
mReadOnlyApnFields = null;
mIsAddApnAllowed = true;
final CarrierConfigManager configManager = (CarrierConfigManager)
context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
if (configManager != null) {
final PersistableBundle b = configManager.getConfigForSubId(mSubId);
if (b != null) {
mReadOnlyApnTypes = b.getStringArray(
CarrierConfigManager.KEY_READ_ONLY_APN_TYPES_STRING_ARRAY);
if (!ArrayUtils.isEmpty(mReadOnlyApnTypes)) {
Log.d(TAG,
"onCreate: read only APN type: " + Arrays.toString(mReadOnlyApnTypes));
}
mReadOnlyApnFields = b.getStringArray(
CarrierConfigManager.KEY_READ_ONLY_APN_FIELDS_STRING_ARRAY);
mDefaultApnTypes = b.getStringArray(
CarrierConfigManager.KEY_APN_SETTINGS_DEFAULT_APN_TYPES_STRING_ARRAY);
if (!ArrayUtils.isEmpty(mDefaultApnTypes)) {
Log.d(TAG, "onCreate: default apn types: " + Arrays.toString(mDefaultApnTypes));
}
mDefaultApnProtocol = b.getString(
CarrierConfigManager.Apn.KEY_SETTINGS_DEFAULT_PROTOCOL_STRING);
if (!TextUtils.isEmpty(mDefaultApnProtocol)) {
Log.d(TAG, "onCreate: default apn protocol: " + mDefaultApnProtocol);
}
mDefaultApnRoamingProtocol = b.getString(
CarrierConfigManager.Apn.KEY_SETTINGS_DEFAULT_ROAMING_PROTOCOL_STRING);
if (!TextUtils.isEmpty(mDefaultApnRoamingProtocol)) {
Log.d(TAG, "onCreate: default apn roaming protocol: "
+ mDefaultApnRoamingProtocol);
}
mIsAddApnAllowed = b.getBoolean(CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL);
if (!mIsAddApnAllowed) {
Log.d(TAG, "onCreate: not allow to add new APN");
}
}
}
}
private void setCarrierCustomizedConfigToUi() {
if (TextUtils.isEmpty(mApnType.getText()) && !ArrayUtils.isEmpty(mDefaultApnTypes)) {
String value = getEditableApnType(mDefaultApnTypes);
mApnType.setText(value);
mApnType.setSummary(value);
}
String protocol = protocolDescription(mDefaultApnProtocol, mProtocol);
if (TextUtils.isEmpty(mProtocol.getValue()) && !TextUtils.isEmpty(protocol)) {
mProtocol.setValue(mDefaultApnProtocol);
mProtocol.setSummary(protocol);
}
String roamingProtocol = protocolDescription(mDefaultApnRoamingProtocol, mRoamingProtocol);
if (TextUtils.isEmpty(mRoamingProtocol.getValue()) && !TextUtils.isEmpty(roamingProtocol)) {
mRoamingProtocol.setValue(mDefaultApnRoamingProtocol);
mRoamingProtocol.setSummary(roamingProtocol);
}
}
/**
* Dialog of error message.
*/
public static class ErrorDialog extends InstrumentedDialogFragment {
/**
* Show error dialog.
*/
public static void showError(ApnEditor editor) {
final ErrorDialog dialog = new ErrorDialog();
dialog.setTargetFragment(editor, 0);
dialog.show(editor.getFragmentManager(), "error");
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final String msg = ((ApnEditor) getTargetFragment()).validateApnData();
return new AlertDialog.Builder(getContext())
.setTitle(R.string.error_title)
.setPositiveButton(android.R.string.ok, null)
.setMessage(msg)
.create();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_APN_EDITOR_ERROR;
}
}
@VisibleForTesting
ApnData getApnDataFromUri(Uri uri) {
ApnData apnData = null;
try (Cursor cursor = getContentResolver().query(
uri,
sProjection,
null /* selection */,
null /* selectionArgs */,
null /* sortOrder */)) {
if (cursor != null && cursor.moveToFirst()) {
apnData = new ApnData(uri, cursor);
}
}
if (apnData == null) {
Log.d(TAG, "Can't get apnData from Uri " + uri);
}
return apnData;
}
@VisibleForTesting
boolean isUserRestricted() {
UserManager userManager = getContext().getSystemService(UserManager.class);
if (userManager == null) {
return false;
}
if (!userManager.isAdminUser()) {
Log.e(TAG, "User is not an admin");
return true;
}
if (userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)) {
Log.e(TAG, "User is not allowed to configure mobile network");
return true;
}
return false;
}
@VisibleForTesting
static class ApnData {
/**
* The uri correspond to a database row of the apn data. This should be null if the apn
* is not in the database.
*/
Uri mUri;
/** Each element correspond to a column of the database row. */
Object[] mData;
ApnData(int numberOfField) {
mData = new Object[numberOfField];
}
ApnData(Uri uri, Cursor cursor) {
mUri = uri;
mData = new Object[cursor.getColumnCount()];
for (int i = 0; i < mData.length; i++) {
switch (cursor.getType(i)) {
case Cursor.FIELD_TYPE_FLOAT:
mData[i] = cursor.getFloat(i);
break;
case Cursor.FIELD_TYPE_INTEGER:
mData[i] = cursor.getInt(i);
break;
case Cursor.FIELD_TYPE_STRING:
mData[i] = cursor.getString(i);
break;
case Cursor.FIELD_TYPE_BLOB:
mData[i] = cursor.getBlob(i);
break;
default:
mData[i] = null;
}
}
}
Uri getUri() {
return mUri;
}
void setUri(Uri uri) {
mUri = uri;
}
Integer getInteger(int index) {
return (Integer) mData[index];
}
Integer getInteger(int index, Integer defaultValue) {
final Integer val = getInteger(index);
return val == null ? defaultValue : val;
}
String getString(int index) {
return (String) mData[index];
}
}
private static int getBitmaskForTech(int radioTech) {
if (radioTech >= 1) {
return (1 << (radioTech - 1));
}
return 0;
}
private static boolean bitmaskHasTech(int bearerBitmask, int radioTech) {
if (bearerBitmask == 0) {
return true;
} else if (radioTech >= 1) {
return ((bearerBitmask & (1 << (radioTech - 1))) != 0);
}
return false;
}
}
ApnEditorTest 源码
packages/apps/Settings/tests/robotests/src/com/android/settings/network/apn/ApnEditorTest.java
/*
* Copyright (C) 2018 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.settings.network.apn;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.PersistableBundle;
import android.os.UserManager;
import android.telephony.CarrierConfigManager;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.MultiSelectListPreference;
import androidx.preference.SwitchPreference;
import com.android.settings.R;
import com.android.settings.network.ProxySubscriptionManager;
import com.android.settings.network.apn.ApnEditor.ApnData;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {
com.android.settings.testutils.shadow.ShadowFragment.class,
})
public class ApnEditorTest {
private static final Object[] APN_DATA = {
0, /* ID */
"apn_name" /* apn name */,
"apn.com" /* apn */,
"" /* proxy */,
"" /* port */,
"" /* username */,
"" /* server */,
"" /* password */,
"" /* MMSC */,
"123" /* MCC */,
"456" /* MNC */,
"123456" /* operator numeric */,
"" /* MMS proxy */,
"" /* MMS port */,
0 /* Authentication type */,
"default,supl,ia" /* APN type */,
"IP" /* APN protocol */,
1 /* APN enable/disable */,
0 /* Bearer */,
0 /* Bearer BITMASK*/,
"IPV6" /* APN roaming protocol */,
"None" /* MVNO type */,
"", /* MVNO value */
};
private static final int CURSOR_INTEGER_INDEX = 0;
private static final int CURSOR_STRING_INDEX = 1;
private static final Uri APN_URI = Uri.parse("Apn://row/1");
@Mock
private Cursor mCursor;
@Mock
private FragmentActivity mActivity;
@Mock
private UserManager mUserManager;
@Mock
private ProxySubscriptionManager mProxySubscriptionMgr;
@Mock
private CarrierConfigManager mCarrierConfigManager;
@Captor
private ArgumentCaptor<Uri> mUriCaptor;
private ApnEditor mApnEditorUT;
private Context mContext;
private Resources mResources;
private PersistableBundle mBundle = new PersistableBundle();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
mResources = mContext.getResources();
mApnEditorUT = spy(new ApnEditor());
doReturn(mActivity).when(mApnEditorUT).getActivity();
doReturn(mResources).when(mApnEditorUT).getResources();
doNothing().when(mApnEditorUT).finish();
doNothing().when(mApnEditorUT).showError();
doReturn(mContext).when(mApnEditorUT).getContext();
doReturn(mContext.getTheme()).when(mActivity).getTheme();
doReturn(mContext.getContentResolver()).when(mActivity).getContentResolver();
doReturn(mUserManager).when(mContext).getSystemService(UserManager.class);
doReturn(true).when(mUserManager).isAdminUser();
doReturn(false).when(mUserManager)
.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
doReturn(mCarrierConfigManager).when(mContext)
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
doReturn(mBundle).when(mCarrierConfigManager).getConfigForSubId(anyInt());
setMockPreference(mContext);
mApnEditorUT.mApnData = new FakeApnData(APN_DATA);
mApnEditorUT.sNotSet = "Not Set";
}
@Test
public void testApnEditor_doesNotUseManagedQuery() {
mApnEditorUT.getApnDataFromUri(Mockito.mock(Uri.class));
verify(mActivity, never()).managedQuery(
any(Uri.class),
any(String[].class),
any(String.class),
any(String.class));
verify(mActivity, never()).managedQuery(
any(Uri.class),
any(String[].class),
any(String.class),
any(String[].class),
any(String.class));
}
@Test
public void getApnDataFromUri_emptyCursor_returnsNull() {
var mockContentResolver = mock(ContentResolver.class);
var mockCursor = mock(Cursor.class);
doReturn(mockContentResolver).when(mActivity).getContentResolver();
when(mockContentResolver.query(any(), any(), any(), any(), any())).thenReturn(mockCursor);
when(mockCursor.moveToFirst()).thenReturn(false);
var apnData = mApnEditorUT.getApnDataFromUri(mock(Uri.class));
assertThat(apnData).isNull();
}
@Test
public void testSetStringValue_valueChanged_shouldSetValue() {
// GIVEN an APN value which is different than the APN value in database
final String apnKey = "apn";
final String apnValue = "testing.com";
final ContentValues cv = new ContentValues();
// WHEN try to check and set the apn value
final boolean isDiff = mApnEditorUT.setStringValueAndCheckIfDiff(
cv, apnKey, apnValue, false /* assumeDiff */, ApnEditor.APN_INDEX);
// THEN the APN value is different than the one in database, and it has been stored in the
// given ContentValues
assertThat(isDiff).isTrue();
assertThat(apnValue).isEqualTo(cv.getAsString(apnKey));
}
@Test
public void testSetStringValue_valueNotChanged_shouldNotSetValue() {
// GIVEN an APN value which is same as the APN value in database
final String apnKey = "apn";
final String apnValue = (String) APN_DATA[ApnEditor.APN_INDEX];
final ContentValues cv = new ContentValues();
// WHEN try to check and set the apn value
final boolean isDiff = mApnEditorUT.setStringValueAndCheckIfDiff(
cv, apnKey, apnValue, false /* assumeDiff */, ApnEditor.APN_INDEX);
// THEN the APN value is same as the one in database, and the new APN value is not stored
// in the given ContentValues
assertThat(isDiff).isFalse();
assertThat(cv.get(apnKey)).isNull();
}
@Test
public void testSetStringValue_nullValue_shouldNotSetValue_shouldNotSetValue() {
// GIVEN a null APN value
final String apnKey = "apn";
final String apnValue = null;
final ContentValues cv = new ContentValues();
// WHEN try to check and set the apn value
final boolean isDiff = mApnEditorUT.setStringValueAndCheckIfDiff(
cv, apnKey, apnValue, false /* assumeDiff */, ApnEditor.APN_INDEX);
// THEN the APN value is different than the one in database, but the null value is not
// stored in the given ContentValues
assertThat(isDiff).isTrue();
assertThat(cv.get(apnKey)).isNull();
}
@Test
public void testSetIntValue_valueChanged_shouldSetValue() {
// GIVEN a value indicated whether the apn is enabled, and it's different than the value in
// the database
final String apnEnabledKey = "apn_enabled";
final int apnEnabledValue = 0;
final ContentValues cv = new ContentValues();
// WHEN try to check and set the apn enabled
final boolean isDiff = mApnEditorUT.setIntValueAndCheckIfDiff(
cv,
apnEnabledKey,
apnEnabledValue,
false /* assumeDiff */,
ApnEditor.CARRIER_ENABLED_INDEX);
// THEN the apn enabled field is different than the one in database, and it has been stored
// in the given ContentValues
assertThat(isDiff).isTrue();
assertThat(cv.getAsInteger(apnEnabledKey)).isEqualTo(apnEnabledValue);
}
@Test
public void testSetIntValue_valueNotChanged_shouldNotSetValue() {
// GIVEN a value indicated whether the apn is enabled, and it's same as the one in the
// database
final String apnEnabledKey = "apn_enabled";
final int apnEnabledValue = (int) APN_DATA[ApnEditor.CARRIER_ENABLED_INDEX];
final ContentValues cv = new ContentValues();
// WHEN try to check and set the apn enabled
final boolean isDiff = mApnEditorUT.setIntValueAndCheckIfDiff(
cv,
apnEnabledKey,
apnEnabledValue,
false /* assumeDiff */,
ApnEditor.CARRIER_ENABLED_INDEX);
// THEN the apn enabled field is same as the one in the database, and the filed is not
// stored in the given ContentValues
assertThat(isDiff).isFalse();
assertThat(cv.get(apnEnabledKey)).isNull();
}
@Test
public void testValidateApnData_validData_shouldReturnNull() {
// GIVEN a valid apn data
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN validate the apn data
final String errMsg = mApnEditorUT.validateApnData();
// THEN the error message should be null
assertThat(errMsg).isNull();
}
@Test
public void testValidateApn_apnNameNotSet_shouldReturnErrorMessage() {
// GIVEN a apn data without the apn name
mApnEditorUT.mApnData.mData[ApnEditor.NAME_INDEX] = "";
mApnEditorUT.fillUI(true /* firstTime */);
// THEN validate the apn data
final String errMsg = mApnEditorUT.validateApnData();
// THEN the error message indicated the apn name not set is returned
assertThat(errMsg).isEqualTo(mResources.getString(R.string.error_name_empty));
}
@Test
public void testValidateApnData_apnNotSet_shouldReturnErrorMessage() {
// GIVEN a apn data without the apn
mApnEditorUT.mApnData.mData[ApnEditor.APN_INDEX] = "";
mApnEditorUT.fillUI(true /* firstTime */);
// THEN validate the apn data
final String errMsg = mApnEditorUT.validateApnData();
// THEN the error message indicated the apn not set is returned
assertThat(errMsg).isEqualTo(mResources.getString(R.string.error_apn_empty));
}
@Test
public void testValidateApnData_mccInvalid_shouldReturnErrorMessage() {
// The length of the mcc should be 3
mApnEditorUT.mApnData.mData[ApnEditor.MCC_INDEX] = "1324";
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN validate the apn data
final String errMsg = mApnEditorUT.validateApnData();
// THEN the error message indicated the mcc invalid is returned
assertThat(errMsg).isEqualTo(mResources.getString(R.string.error_mcc_not3));
}
@Test
public void testValidateApnData_mncInvalid_shouldReturnErrorMessage() {
// GIVEN an apn data with invalid mnc
// The length of the mnc should be 2 or 3
mApnEditorUT.mApnData.mData[ApnEditor.MNC_INDEX] = "1324";
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN validate the apn data
final String errMsg = mApnEditorUT.validateApnData();
// THEN the error message indicated the mnc invalid is returned
assertThat(errMsg).isEqualTo(mResources.getString(R.string.error_mnc_not23));
}
@Test
public void testSaveApnData_pressBackButtonWithValidApnData_shouldSaveApnData() {
// GIVEN a valid apn data
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN press the back button
final KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
mApnEditorUT.onKey(new View(mContext), KeyEvent.KEYCODE_BACK, event);
// THEN the apn data is saved and the apn editor is closed
verify(mApnEditorUT).validateAndSaveApnData();
verify(mApnEditorUT).finish();
}
@Test
public void testSaveApnData_pressSaveButtonWithValidApnData_shouldSaveApnData() {
// GIVEN a valid apn data
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN press the save button
MenuItem item = Mockito.mock(MenuItem.class);
// Menu.FIRST + 1 indicated the SAVE option in ApnEditor
doReturn(Menu.FIRST + 1).when(item).getItemId();
mApnEditorUT.onOptionsItemSelected(item);
// THEN the apn data is saved and the apn editor is closed
verify(mApnEditorUT).validateAndSaveApnData();
verify(mApnEditorUT).finish();
}
@Test
public void testSaveApnData_apnDataInvalid_shouldNotSaveApnData() {
// GIVEN an invalid apn data
// The valid apn data should contains a non-empty apn name
mApnEditorUT.mApnData.mData[ApnEditor.NAME_INDEX] = "";
mApnEditorUT.fillUI(true /* firstTime */);
// WHEN press the save button
final MenuItem item = Mockito.mock(MenuItem.class);
// Menu.FIRST + 1 indicated the SAVE option in ApnEditor
doReturn(Menu.FIRST + 1).when(item).getItemId();
mApnEditorUT.onOptionsItemSelected(item);
// THEN the error dialog is shown
verify(mApnEditorUT).validateAndSaveApnData();
verify(mApnEditorUT).showError();
}
@Test
public void testDeleteApnData_shouldDeleteData() {
// GIVEN a valid apn data correspond a row in database
final Uri apnUri = Uri.parse("content://telephony/carriers/1");
mApnEditorUT.mApnData = new FakeApnData(APN_DATA, apnUri);
mApnEditorUT.fillUI(true /* firstTime */);
ContentResolver mockContentResolver = Mockito.mock(ContentResolver.class);
doReturn(mockContentResolver).when(mActivity).getContentResolver();
// WHEN press the save button
final MenuItem item = Mockito.mock(MenuItem.class);
// Menu.FIRST indicated the DELETE option in ApnEditor
doReturn(Menu.FIRST).when(item).getItemId();
mApnEditorUT.onOptionsItemSelected(item);
// THEN the apn data is deleted and the apn editor is closed
verify(mockContentResolver).delete(mUriCaptor.capture(), any(), any());
assertThat(apnUri).isEqualTo(mUriCaptor.getValue());
verify(mApnEditorUT).finish();
}
@Test
public void testDeleteApnData_shouldNotPresentMenuWhenNotSupportAdding() {
mBundle.putBoolean(CarrierConfigManager.KEY_ALLOW_ADDING_APNS_BOOL, false);
MenuItem item = Mockito.mock(MenuItem.class);
Menu menu = Mockito.mock(Menu.class);
doReturn(item).when(menu).add(anyInt(), anyInt(), anyInt(), anyInt());
mApnEditorUT.getCarrierCustomizedConfig(mContext);
mApnEditorUT.onCreateOptionsMenu(menu, null);
verify(menu, times(0)).add(anyInt(), eq(ApnEditor.MENU_DELETE), anyInt(), anyInt());
}
@Test(expected = ClassCastException.class)
public void testApnData_invalidIntegerType_throwsInvalidTypeException() {
// GIVEN a ApnData constructed from cursor
initCursor();
final ApnData data = new ApnData(APN_URI, mCursor);
// WHEN get a string from an integer column
// THEN the InvalidTypeException is threw
data.getString(CURSOR_INTEGER_INDEX);
}
@Test(expected = ClassCastException.class)
public void testApnData_invalidStringType_throwsInvalidTypeException() {
// GIVEN a ApnData constructed from cursor
initCursor();
final ApnData data = new ApnData(APN_URI, mCursor);
// WHEN get a integer from a string column
// THEN the InvalidTypeException is threw
data.getInteger(CURSOR_STRING_INDEX);
}
@Test
public void testApnData_validIntegerType_returnCorrectValue() {
// GIVEN a ApnData constructed from cursor
initCursor();
final ApnData data = new ApnData(APN_URI, mCursor);
// WHEN get integer from an integer column
final int val = data.getInteger(CURSOR_INTEGER_INDEX);
// THEN the integer is returned correctly
assertThat(val).isEqualTo(mCursor.getInt(CURSOR_INTEGER_INDEX));
}
@Test
public void testApnData_validStringType_returnCorrectValue() {
// GIVEN a ApnData constructed from cursor
initCursor();
final ApnData data = new ApnData(APN_URI, mCursor);
// WHEN get string from a string column
final String str = data.getString(CURSOR_STRING_INDEX);
// THEN the integer is returned correctly
assertThat(str).isEqualTo(mCursor.getString(CURSOR_STRING_INDEX));
}
@Test
public void testApnData_nullValueColumn_returnNull() {
// GIVEN a empty ApnData
final ApnData data = new ApnData(3);
// WHEN get string value from a null column
final String str = data.getString(0);
// THEN the null value is returned
assertThat(str).isNull();
}
@Test
public void formatInteger_shouldParseString() {
assertThat(ApnEditor.formatInteger("42")).isEqualTo("42");
assertThat(ApnEditor.formatInteger("01")).isEqualTo("01");
assertThat(ApnEditor.formatInteger("001")).isEqualTo("001");
}
@Test
public void formatInteger_shouldIgnoreNonIntegers() {
assertThat(ApnEditor.formatInteger("not an int")).isEqualTo("not an int");
}
@Test
public void onCreate_notAdminUser_shouldFinish() {
doReturn(false).when(mUserManager).isAdminUser();
mApnEditorUT.onCreate(null);
verify(mApnEditorUT).finish();
}
@Test
public void onCreate_hasUserRestriction_shouldFinish() {
doReturn(true).when(mUserManager)
.hasUserRestriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS);
mApnEditorUT.onCreate(null);
verify(mApnEditorUT).finish();
}
@Test
public void onCreate_noAction_shouldFinishAndNoCrash() {
ProxySubscriptionManager proxySubscriptionMgr = mock(ProxySubscriptionManager.class);
mApnEditorUT.mProxySubscriptionMgr = proxySubscriptionMgr;
doReturn(new Intent()).when(mActivity).getIntent();
doNothing().when(mApnEditorUT).addPreferencesFromResource(anyInt());
mApnEditorUT.onCreate(null);
verify(mApnEditorUT).finish();
}
@Test
public void testOnViewStateRestored_customizedValueWithoutDefault_shouldShowCustomized() {
mApnEditorUT.mDefaultApnProtocol = "IP";
mApnEditorUT.mApnData.mData[ApnEditor.PROTOCOL_INDEX] = null;
mApnEditorUT.mProtocol.setEntryValues(new CharSequence[]{"IP", "IPV6", "IPV4V6"});
mApnEditorUT.onViewStateRestored(null);
assertThat(mApnEditorUT.mProtocol.getSummary()).isEqualTo("IPv4");
}
@Test
public void testOnViewStateRestored_customizedValueWithDefault_shouldShowDefault() {
mApnEditorUT.mDefaultApnProtocol = "IP";
mApnEditorUT.mApnData.mData[ApnEditor.PROTOCOL_INDEX] = "IPV6";
mApnEditorUT.mProtocol.setEntryValues(new CharSequence[]{"IP", "IPV6", "IPV4V6"});
mApnEditorUT.onViewStateRestored(null);
assertThat(mApnEditorUT.mProtocol.getSummary()).isEqualTo("IPv6");
}
@Test
public void getUserEnteredApnType_emptyApnType_shouldReturnDefault() {
// case 1
// GIVEN read only APN types with DUN
mApnEditorUT.mReadOnlyApnTypes = new String [] {"dun"};
// GIVEN read specificApnTypeForEmptyInput with DEFAULT,DUN
mApnEditorUT.mDefaultApnTypes = new String [] {"default", "dun"};
// Input empty in TYPE
mApnEditorUT.mApnData.mData[ApnEditor.TYPE_INDEX] = "";
mApnEditorUT.onViewStateRestored(null);
// THEN APN type should be default
assertThat(mApnEditorUT.getUserEnteredApnType()).isEqualTo("default");
// case 2
// GIVEN read only APN types with DUN
mApnEditorUT.mReadOnlyApnTypes = new String [] {"dun"};
// GIVEN read specificApnTypeForEmptyInput with DEFAULT
mApnEditorUT.mDefaultApnTypes = new String [] {"default"};
// Input empty in TYPE
mApnEditorUT.mApnData.mData[ApnEditor.TYPE_INDEX] = "";
mApnEditorUT.onViewStateRestored(null);
// THEN APN type should be default
assertThat(mApnEditorUT.getUserEnteredApnType()).isEqualTo("default");
}
private void initCursor() {
doReturn(2).when(mCursor).getColumnCount();
doReturn(2).when(mCursor).getInt(CURSOR_INTEGER_INDEX);
doReturn("str").when(mCursor).getString(CURSOR_STRING_INDEX);
doReturn(Cursor.FIELD_TYPE_INTEGER).when(mCursor).getType(CURSOR_INTEGER_INDEX);
doReturn(Cursor.FIELD_TYPE_STRING).when(mCursor).getType(CURSOR_STRING_INDEX);
}
private void setMockPreference(Context context) {
mApnEditorUT.mName = new EditTextPreference(context);
mApnEditorUT.mApn = new EditTextPreference(context);
mApnEditorUT.mProxy = new EditTextPreference(context);
mApnEditorUT.mPort = new EditTextPreference(context);
mApnEditorUT.mUser = new EditTextPreference(context);
mApnEditorUT.mServer = new EditTextPreference(context);
mApnEditorUT.mPassword = new EditTextPreference(context);
mApnEditorUT.mMmsc = new EditTextPreference(context);
mApnEditorUT.mMcc = new EditTextPreference(context);
mApnEditorUT.mMnc = new EditTextPreference(context);
mApnEditorUT.mMmsProxy = new EditTextPreference(context);
mApnEditorUT.mMmsPort = new EditTextPreference(context);
mApnEditorUT.mAuthType = new ListPreference(context);
mApnEditorUT.mApnType = new EditTextPreference(context);
mApnEditorUT.mProtocol = new ListPreference(context);
mApnEditorUT.mRoamingProtocol = new ListPreference(context);
mApnEditorUT.mCarrierEnabled = new SwitchPreference(context);
mApnEditorUT.mBearerMulti = new MultiSelectListPreference(context);
mApnEditorUT.mMvnoType = new ListPreference(context);
mApnEditorUT.mMvnoMatchData = new EditTextPreference(context);
}
private final class FakeApnData extends ApnData {
FakeApnData(Object[] data) {
super(data.length);
System.arraycopy(data, 0, mData, 0, data.length);
}
FakeApnData(Object[] data, Uri uri) {
this(data);
mUri = uri;
}
}
}
参考资料
应用
Settings 应用介绍
Android UI
Android 源码Fragment 基类
关于Android 8.0 为“设置”应用引入了全新的信息架构,包含DashboardFragment 、 PreferenceController 等通用基类