AOSP 应用 | com.android.settings.network APN设置界面代码和测试

主界面源码

ApnSettings.java 源码(Settings应用)

packages/apps/Settings/src/com/android/settings/network/apn/ApnSettings.java

main 版本
main 版本
/*
 * 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的。
 ApnSettings 变量列表
ApnSettings 变量列表

  • M标识方法接口,紫色填充M是 public ,无填充紫色M是 private 。
ApnSettings 接口列表
ApnSettings 接口列表

API 接口汇总列表

需要评估单元测试可行性,即接口可测试性。

Note:以下列出AOSP源码定义的接口,不含OEM定制的逻辑实现。

ApnSettings Methods
AnnotationFunction methodAPI功能Num
/public ApnSettings()ApnSettings构造方法1
@Overridepublic int getMetricsCategory()getMetricsCategory2
@Overridepublic void onCreate(Bundle icicle)onCreate界面构建3
@Overridepublic void onActivityCreated(Bundle savedInstanceState)onActivityCreated4
@Overridepublic void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState)onViewCreated5
@Overridepublic void onResume()onResume6
@Overridepublic EnforcedAdmin getRestrictionEnforcedAdmin()getRestrictionEnforcedAdmin7
/private void fillList()fillList查询数据库,加载APN列表8
@Overridepublic void onCreateOptionsMenu(Menu menu, MenuInflater inflater)onCreateOptionsMenu9
@Overridepublic boolean onOptionsItemSelected(MenuItem item)onOptionsItemSelected菜单MENU_NEW MENU_RESTORE10
/private void addNewApn()addNewApn添加APN,跳转ApnEditor11
@Overridepublic boolean onPreferenceChange(Preference preference, Object newValue)onPreferenceChange12
/private void restoreDefaultApn()restoreDefaultApn重置APN13
/private void onPreferredApnRestored()onPreferredApnRestored14
@Overridepublic Dialog onCreateDialog(int id)onCreateDialog15
@Overridepublic int getDialogMetricsCategory(int dialogId)getDialogMetricsCategory16

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 逻辑控制。

ApnPreferenceController 方法和 AndroidJUnit4 测试用例设计
代码接口功能描述@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 Settings解析-腾讯云开发者社区-腾讯云Android设置应用是Android系统中一个非常重要的系统应用,它允许用户调整和设置系统的各种参数和功能(系统设置/自定义设置/控制应用权限/开发者选项/系统信息等),使用户获得更好的使用体验。同时它一般也是Android系统开发者了解深入的第一个系统级应用,也是用户使用最频繁的系统应用。icon-default.png?t=O83Ahttps://cloud.tencent.com/developer/article/2349589

Android UI

Android 源码Fragment 基类

关于Android 8.0 为“设置”应用引入了全新的信息架构,包含DashboardFragmentPreferenceController 等通用基类

信息架构  |  Android Open Source Projecticon-default.png?t=O83Ahttps://source.android.google.cn/docs/core/settings/info-architecture?hl=zh-cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值