1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.policy.impl.keyguard_obsolete;
18
19import com.android.internal.R;
20import com.android.internal.telephony.IccCardConstants;
21import com.android.internal.widget.DigitalClock;
22import com.android.internal.widget.LockPatternUtils;
23import com.android.internal.widget.TransportControlView;
24
25import java.util.ArrayList;
26import java.util.Date;
27
28import libcore.util.MutableInt;
29
30import android.content.ContentResolver;
31import android.content.Context;
32import android.provider.Settings;
33import android.text.TextUtils;
34import android.text.format.DateFormat;
35import android.util.Log;
36import android.view.View;
37import android.view.View.OnClickListener;
38import android.widget.Button;
39import android.widget.TextView;
40
41/***
42 * Manages a number of views inside of LockScreen layouts. See below for a list of widgets
43 *
44 */
45class KeyguardStatusViewManager implements OnClickListener {
46    private static final boolean DEBUG = false;
47    private static final String TAG = "KeyguardStatusView";
48
49    public static final int LOCK_ICON = 0; // R.drawable.ic_lock_idle_lock;
50    public static final int ALARM_ICON = R.drawable.ic_lock_idle_alarm;
51    public static final int CHARGING_ICON = 0; //R.drawable.ic_lock_idle_charging;
52    public static final int BATTERY_LOW_ICON = 0; //R.drawable.ic_lock_idle_low_battery;
53    private static final long INSTRUCTION_RESET_DELAY = 2000; // time until instruction text resets
54
55    private static final int INSTRUCTION_TEXT = 10;
56    private static final int CARRIER_TEXT = 11;
57    private static final int CARRIER_HELP_TEXT = 12;
58    private static final int HELP_MESSAGE_TEXT = 13;
59    private static final int OWNER_INFO = 14;
60    private static final int BATTERY_INFO = 15;
61
62    private StatusMode mStatus;
63    private String mDateFormatString;
64    private TransientTextManager mTransientTextManager;
65
66    // Views that this class controls.
67    // NOTE: These may be null in some LockScreen screens and should protect from NPE
68    private TextView mCarrierView;
69    private TextView mDateView;
70    private TextView mStatus1View;
71    private TextView mOwnerInfoView;
72    private TextView mAlarmStatusView;
73    private TransportControlView mTransportView;
74
75    // Top-level container view for above views
76    private View mContainer;
77
78    // are we showing battery information?
79    private boolean mShowingBatteryInfo = false;
80
81    // last known plugged in state
82    private boolean mPluggedIn = false;
83
84    // last known battery level
85    private int mBatteryLevel = 100;
86
87    // last known SIM state
88    protected IccCardConstants.State mSimState;
89
90    private LockPatternUtils mLockPatternUtils;
91    private KeyguardUpdateMonitor mUpdateMonitor;
92    private Button mEmergencyCallButton;
93    private boolean mEmergencyButtonEnabledBecauseSimLocked;
94
95    // Shadowed text values
96    private CharSequence mCarrierText;
97    private CharSequence mCarrierHelpText;
98    private String mHelpMessageText;
99    private String mInstructionText;
100    private CharSequence mOwnerInfoText;
101    private boolean mShowingStatus;
102    private KeyguardScreenCallback mCallback;
103    private final boolean mEmergencyCallButtonEnabledInScreen;
104    private CharSequence mPlmn;
105    private CharSequence mSpn;
106    protected int mPhoneState;
107    private DigitalClock mDigitalClock;
108    protected boolean mBatteryCharged;
109    protected boolean mBatteryIsLow;
110
111    private class TransientTextManager {
112        private TextView mTextView;
113        private class Data {
114            final int icon;
115            final CharSequence text;
116            Data(CharSequence t, int i) {
117                text = t;
118                icon = i;
119            }
120        };
121        private ArrayList<Data> mMessages = new ArrayList<Data>(5);
122
123        TransientTextManager(TextView textView) {
124            mTextView = textView;
125        }
126
127        /* Show given message with icon for up to duration ms. Newer messages override older ones.
128         * The most recent message with the longest duration is shown as messages expire until
129         * nothing is left, in which case the text/icon is defined by a call to
130         * getAltTextMessage() */
131        void post(final CharSequence message, final int icon, long duration) {
132            if (mTextView == null) {
133                return;
134            }
135            mTextView.setText(message);
136            mTextView.setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
137            final Data data = new Data(message, icon);
138            mContainer.postDelayed(new Runnable() {
139                public void run() {
140                    mMessages.remove(data);
141                    int last = mMessages.size() - 1;
142                    final CharSequence lastText;
143                    final int lastIcon;
144                    if (last > 0) {
145                        final Data oldData = mMessages.get(last);
146                        lastText = oldData.text;
147                        lastIcon = oldData.icon;
148                    } else {
149                        final MutableInt tmpIcon = new MutableInt(0);
150                        lastText = getAltTextMessage(tmpIcon);
151                        lastIcon = tmpIcon.value;
152                    }
153                    mTextView.setText(lastText);
154                    mTextView.setCompoundDrawablesWithIntrinsicBounds(lastIcon, 0, 0, 0);
155                }
156            }, duration);
157        }
158    };
159
160    /**
161     *
162     * @param view the containing view of all widgets
163     * @param updateMonitor the update monitor to use
164     * @param lockPatternUtils lock pattern util object
165     * @param callback used to invoke emergency dialer
166     * @param emergencyButtonEnabledInScreen whether emergency button is enabled by default
167     */
168    public KeyguardStatusViewManager(View view, KeyguardUpdateMonitor updateMonitor,
169                LockPatternUtils lockPatternUtils, KeyguardScreenCallback callback,
170                boolean emergencyButtonEnabledInScreen) {
171        if (DEBUG) Log.v(TAG, "KeyguardStatusViewManager()");
172        mContainer = view;
173        mDateFormatString = getContext().getString(R.string.abbrev_wday_month_day_no_year);
174        mLockPatternUtils = lockPatternUtils;
175        mUpdateMonitor = updateMonitor;
176        mCallback = callback;
177
178        mCarrierView = (TextView) findViewById(R.id.carrier);
179        mDateView = (TextView) findViewById(R.id.date);
180        mStatus1View = (TextView) findViewById(R.id.status1);
181        mAlarmStatusView = (TextView) findViewById(R.id.alarm_status);
182        mOwnerInfoView = (TextView) findViewById(R.id.owner_info);
183        mTransportView = (TransportControlView) findViewById(R.id.transport);
184        mEmergencyCallButton = (Button) findViewById(R.id.emergencyCallButton);
185        mEmergencyCallButtonEnabledInScreen = emergencyButtonEnabledInScreen;
186        mDigitalClock = (DigitalClock) findViewById(R.id.time);
187
188        // Hide transport control view until we know we need to show it.
189        if (mTransportView != null) {
190            mTransportView.setVisibility(View.GONE);
191        }
192
193        if (mEmergencyCallButton != null) {
194            mEmergencyCallButton.setText(R.string.lockscreen_emergency_call);
195            mEmergencyCallButton.setOnClickListener(this);
196            mEmergencyCallButton.setFocusable(false); // touch only!
197        }
198
199        mTransientTextManager = new TransientTextManager(mCarrierView);
200
201        // Registering this callback immediately updates the battery state, among other things.
202        mUpdateMonitor.registerCallback(mInfoCallback);
203
204        resetStatusInfo();
205        refreshDate();
206        updateOwnerInfo();
207
208        // Required to get Marquee to work.
209        final View scrollableViews[] = { mCarrierView, mDateView, mStatus1View, mOwnerInfoView,
210                mAlarmStatusView };
211        for (View v : scrollableViews) {
212            if (v != null) {
213                v.setSelected(true);
214            }
215        }
216    }
217
218    private boolean inWidgetMode() {
219        return mTransportView != null && mTransportView.getVisibility() == View.VISIBLE;
220    }
221
222    void setInstructionText(String string) {
223        mInstructionText = string;
224        update(INSTRUCTION_TEXT, string);
225    }
226
227    void setCarrierText(CharSequence string) {
228        mCarrierText = string;
229        update(CARRIER_TEXT, string);
230    }
231
232    void setOwnerInfo(CharSequence string) {
233        mOwnerInfoText = string;
234        update(OWNER_INFO, string);
235    }
236
237    /**
238     * Sets the carrier help text message, if view is present. Carrier help text messages are
239     * typically for help dealing with SIMS and connectivity.
240     *
241     * @param resId resource id of the message
242     */
243    public void setCarrierHelpText(int resId) {
244        mCarrierHelpText = getText(resId);
245        update(CARRIER_HELP_TEXT, mCarrierHelpText);
246    }
247
248    private CharSequence getText(int resId) {
249        return resId == 0 ? null : getContext().getText(resId);
250    }
251
252    /**
253     * Unlock help message.  This is typically for help with unlock widgets, e.g. "wrong password"
254     * or "try again."
255     *
256     * @param textResId
257     * @param lockIcon
258     */
259    public void setHelpMessage(int textResId, int lockIcon) {
260        final CharSequence tmp = getText(textResId);
261        mHelpMessageText = tmp == null ? null : tmp.toString();
262        update(HELP_MESSAGE_TEXT, mHelpMessageText);
263    }
264
265    private void update(int what, CharSequence string) {
266        if (inWidgetMode()) {
267            if (DEBUG) Log.v(TAG, "inWidgetMode() is true");
268            // Use Transient text for messages shown while widget is shown.
269            switch (what) {
270                case INSTRUCTION_TEXT:
271                case CARRIER_HELP_TEXT:
272                case HELP_MESSAGE_TEXT:
273                case BATTERY_INFO:
274                    mTransientTextManager.post(string, 0, INSTRUCTION_RESET_DELAY);
275                    break;
276
277                case OWNER_INFO:
278                case CARRIER_TEXT:
279                default:
280                    if (DEBUG) Log.w(TAG, "Not showing message id " + what + ", str=" + string);
281            }
282        } else {
283            updateStatusLines(mShowingStatus);
284        }
285    }
286
287    public void onPause() {
288        if (DEBUG) Log.v(TAG, "onPause()");
289        mUpdateMonitor.removeCallback(mInfoCallback);
290    }
291
292    /** {@inheritDoc} */
293    public void onResume() {
294        if (DEBUG) Log.v(TAG, "onResume()");
295
296        // First update the clock, if present.
297        if (mDigitalClock != null) {
298            mDigitalClock.updateTime();
299        }
300
301        mUpdateMonitor.registerCallback(mInfoCallback);
302        resetStatusInfo();
303        // Issue the biometric unlock failure message in a centralized place
304        // TODO: we either need to make the Face Unlock multiple failures string a more general
305        // 'biometric unlock' or have each biometric unlock handle this on their own.
306        if (mUpdateMonitor.getMaxBiometricUnlockAttemptsReached()) {
307            setInstructionText(getContext().getString(R.string.faceunlock_multiple_failures));
308        }
309    }
310
311    void resetStatusInfo() {
312        mInstructionText = null;
313        updateStatusLines(true);
314    }
315
316    /**
317     * Update the status lines based on these rules:
318     * AlarmStatus: Alarm state always gets it's own line.
319     * Status1 is shared between help, battery status and generic unlock instructions,
320     * prioritized in that order.
321     * @param showStatusLines status lines are shown if true
322     */
323    void updateStatusLines(boolean showStatusLines) {
324        if (DEBUG) Log.v(TAG, "updateStatusLines(" + showStatusLines + ")");
325        mShowingStatus = showStatusLines;
326        updateAlarmInfo();
327        updateOwnerInfo();
328        updateStatus1();
329        updateCarrierText();
330    }
331
332    private void updateAlarmInfo() {
333        if (mAlarmStatusView != null) {
334            String nextAlarm = mLockPatternUtils.getNextAlarm();
335            boolean showAlarm = mShowingStatus && !TextUtils.isEmpty(nextAlarm);
336            mAlarmStatusView.setText(nextAlarm);
337            mAlarmStatusView.setCompoundDrawablesWithIntrinsicBounds(ALARM_ICON, 0, 0, 0);
338            mAlarmStatusView.setVisibility(showAlarm ? View.VISIBLE : View.GONE);
339        }
340    }
341
342    private void updateOwnerInfo() {
343        final ContentResolver res = getContext().getContentResolver();
344        final boolean ownerInfoEnabled = Settings.Secure.getInt(res,
345                Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, 1) != 0;
346        mOwnerInfoText = ownerInfoEnabled ?
347                Settings.Secure.getString(res, Settings.Secure.LOCK_SCREEN_OWNER_INFO) : null;
348        if (mOwnerInfoView != null) {
349            mOwnerInfoView.setText(mOwnerInfoText);
350            mOwnerInfoView.setVisibility(TextUtils.isEmpty(mOwnerInfoText) ? View.GONE:View.VISIBLE);
351        }
352    }
353
354    private void updateStatus1() {
355        if (mStatus1View != null) {
356            MutableInt icon = new MutableInt(0);
357            CharSequence string = getPriorityTextMessage(icon);
358            mStatus1View.setText(string);
359            mStatus1View.setCompoundDrawablesWithIntrinsicBounds(icon.value, 0, 0, 0);
360            mStatus1View.setVisibility(mShowingStatus ? View.VISIBLE : View.INVISIBLE);
361        }
362    }
363
364    private void updateCarrierText() {
365        if (!inWidgetMode() && mCarrierView != null) {
366            mCarrierView.setText(mCarrierText);
367        }
368    }
369
370    private CharSequence getAltTextMessage(MutableInt icon) {
371        // If we have replaced the status area with a single widget, then this code
372        // prioritizes what to show in that space when all transient messages are gone.
373        CharSequence string = null;
374        if (mShowingBatteryInfo) {
375            // Battery status
376            if (mPluggedIn) {
377                // Charging, charged or waiting to charge.
378                string = getContext().getString(mBatteryCharged ? R.string.lockscreen_charged
379                        :R.string.lockscreen_plugged_in, mBatteryLevel);
380                icon.value = CHARGING_ICON;
381            } else if (mBatteryIsLow) {
382                // Battery is low
383                string = getContext().getString(R.string.lockscreen_low_battery);
384                icon.value = BATTERY_LOW_ICON;
385            }
386        } else {
387            string = mCarrierText;
388        }
389        return string;
390    }
391
392    private CharSequence getPriorityTextMessage(MutableInt icon) {
393        CharSequence string = null;
394        if (!TextUtils.isEmpty(mInstructionText)) {
395            // Instructions only
396            string = mInstructionText;
397            icon.value = LOCK_ICON;
398        } else if (mShowingBatteryInfo) {
399            // Battery status
400            if (mPluggedIn) {
401                // Charging, charged or waiting to charge.
402                string = getContext().getString(mBatteryCharged ? R.string.lockscreen_charged
403                        :R.string.lockscreen_plugged_in, mBatteryLevel);
404                icon.value = CHARGING_ICON;
405            } else if (mBatteryIsLow) {
406                // Battery is low
407                string = getContext().getString(R.string.lockscreen_low_battery);
408                icon.value = BATTERY_LOW_ICON;
409            }
410        } else if (!inWidgetMode() && mOwnerInfoView == null && mOwnerInfoText != null) {
411            // OwnerInfo shows in status if we don't have a dedicated widget
412            string = mOwnerInfoText;
413        }
414        return string;
415    }
416
417    void refreshDate() {
418        if (mDateView != null) {
419            mDateView.setText(DateFormat.format(mDateFormatString, new Date()));
420        }
421    }
422
423    /**
424     * Determine the current status of the lock screen given the sim state and other stuff.
425     */
426    public StatusMode getStatusForIccState(IccCardConstants.State simState) {
427        // Since reading the SIM may take a while, we assume it is present until told otherwise.
428        if (simState == null) {
429            return StatusMode.Normal;
430        }
431
432        final boolean missingAndNotProvisioned = (!mUpdateMonitor.isDeviceProvisioned()
433                && (simState == IccCardConstants.State.ABSENT ||
434                        simState == IccCardConstants.State.PERM_DISABLED));
435
436        // Assume we're NETWORK_LOCKED if not provisioned
437        simState = missingAndNotProvisioned ? IccCardConstants.State.NETWORK_LOCKED : simState;
438        switch (simState) {
439            case ABSENT:
440                return StatusMode.SimMissing;
441            case NETWORK_LOCKED:
442                return StatusMode.SimMissingLocked;
443            case NOT_READY:
444                return StatusMode.SimMissing;
445            case PIN_REQUIRED:
446                return StatusMode.SimLocked;
447            case PUK_REQUIRED:
448                return StatusMode.SimPukLocked;
449            case READY:
450                return StatusMode.Normal;
451            case PERM_DISABLED:
452                return StatusMode.SimPermDisabled;
453            case UNKNOWN:
454                return StatusMode.SimMissing;
455        }
456        return StatusMode.SimMissing;
457    }
458
459    private Context getContext() {
460        return mContainer.getContext();
461    }
462
463    /**
464     * Update carrier text, carrier help and emergency button to match the current status based
465     * on SIM state.
466     *
467     * @param simState
468     */
469    private void updateCarrierStateWithSimStatus(IccCardConstants.State simState) {
470        if (DEBUG) Log.d(TAG, "updateCarrierTextWithSimStatus(), simState = " + simState);
471
472        CharSequence carrierText = null;
473        int carrierHelpTextId = 0;
474        mEmergencyButtonEnabledBecauseSimLocked = false;
475        mStatus = getStatusForIccState(simState);
476        mSimState = simState;
477        switch (mStatus) {
478            case Normal:
479                carrierText = makeCarierString(mPlmn, mSpn);
480                break;
481
482            case NetworkLocked:
483                carrierText = makeCarrierStringOnEmergencyCapable(
484                        getContext().getText(R.string.lockscreen_network_locked_message),
485                        mPlmn);
486                carrierHelpTextId = R.string.lockscreen_instructions_when_pattern_disabled;
487                break;
488
489            case SimMissing:
490                // Shows "No SIM card | Emergency calls only" on devices that are voice-capable.
491                // This depends on mPlmn containing the text "Emergency calls only" when the radio
492                // has some connectivity. Otherwise, it should be null or empty and just show
493                // "No SIM card"
494                carrierText =  makeCarrierStringOnEmergencyCapable(
495                        getContext().getText(R.string.lockscreen_missing_sim_message_short),
496                        mPlmn);
497                carrierHelpTextId = R.string.lockscreen_missing_sim_instructions_long;
498                break;
499
500            case SimPermDisabled:
501                carrierText = getContext().getText(
502                        R.string.lockscreen_permanent_disabled_sim_message_short);
503                carrierHelpTextId = R.string.lockscreen_permanent_disabled_sim_instructions;
504                mEmergencyButtonEnabledBecauseSimLocked = true;
505                break;
506
507            case SimMissingLocked:
508                carrierText =  makeCarrierStringOnEmergencyCapable(
509                        getContext().getText(R.string.lockscreen_missing_sim_message_short),
510                        mPlmn);
511                carrierHelpTextId = R.string.lockscreen_missing_sim_instructions;
512                mEmergencyButtonEnabledBecauseSimLocked = true;
513                break;
514
515            case SimLocked:
516                carrierText = makeCarrierStringOnEmergencyCapable(
517                        getContext().getText(R.string.lockscreen_sim_locked_message),
518                        mPlmn);
519                mEmergencyButtonEnabledBecauseSimLocked = true;
520                break;
521
522            case SimPukLocked:
523                carrierText = makeCarrierStringOnEmergencyCapable(
524                        getContext().getText(R.string.lockscreen_sim_puk_locked_message),
525                        mPlmn);
526                if (!mLockPatternUtils.isPukUnlockScreenEnable()) {
527                    // This means we're showing the PUK unlock screen
528                    mEmergencyButtonEnabledBecauseSimLocked = true;
529                }
530                break;
531        }
532
533        setCarrierText(carrierText);
534        setCarrierHelpText(carrierHelpTextId);
535        updateEmergencyCallButtonState(mPhoneState);
536    }
537
538
539    /*
540     * Add emergencyCallMessage to carrier string only if phone supports emergency calls.
541     */
542    private CharSequence makeCarrierStringOnEmergencyCapable(
543            CharSequence simMessage, CharSequence emergencyCallMessage) {
544        if (mLockPatternUtils.isEmergencyCallCapable()) {
545            return makeCarierString(simMessage, emergencyCallMessage);
546        }
547        return simMessage;
548    }
549
550    private View findViewById(int id) {
551        return mContainer.findViewById(id);
552    }
553
554    /**
555     * The status of this lock screen. Primarily used for widgets on LockScreen.
556     */
557    enum StatusMode {
558        /**
559         * Normal case (sim card present, it's not locked)
560         */
561        Normal(true),
562
563        /**
564         * The sim card is 'network locked'.
565         */
566        NetworkLocked(true),
567
568        /**
569         * The sim card is missing.
570         */
571        SimMissing(false),
572
573        /**
574         * The sim card is missing, and this is the device isn't provisioned, so we don't let
575         * them get past the screen.
576         */
577        SimMissingLocked(false),
578
579        /**
580         * The sim card is PUK locked, meaning they've entered the wrong sim unlock code too many
581         * times.
582         */
583        SimPukLocked(false),
584
585        /**
586         * The sim card is locked.
587         */
588        SimLocked(true),
589
590        /**
591         * The sim card is permanently disabled due to puk unlock failure
592         */
593        SimPermDisabled(false);
594
595        private final boolean mShowStatusLines;
596
597        StatusMode(boolean mShowStatusLines) {
598            this.mShowStatusLines = mShowStatusLines;
599        }
600
601        /**
602         * @return Whether the status lines (battery level and / or next alarm) are shown while
603         *         in this state.  Mostly dictated by whether this is room for them.
604         */
605        public boolean shouldShowStatusLines() {
606            return mShowStatusLines;
607        }
608    }
609
610    private void updateEmergencyCallButtonState(int phoneState) {
611        if (mEmergencyCallButton != null) {
612            boolean enabledBecauseSimLocked =
613                    mLockPatternUtils.isEmergencyCallEnabledWhileSimLocked()
614                    && mEmergencyButtonEnabledBecauseSimLocked;
615            boolean shown = mEmergencyCallButtonEnabledInScreen || enabledBecauseSimLocked;
616            mLockPatternUtils.updateEmergencyCallButtonState(mEmergencyCallButton,
617                    phoneState, shown);
618        }
619    }
620
621    private KeyguardUpdateMonitorCallback mInfoCallback = new KeyguardUpdateMonitorCallback() {
622
623        public void onRefreshBatteryInfo(KeyguardUpdateMonitor.BatteryStatus status) {
624            mShowingBatteryInfo = status.isPluggedIn() || status.isBatteryLow();
625            mPluggedIn = status.isPluggedIn();
626            mBatteryLevel = status.level;
627            mBatteryCharged = status.isCharged();
628            mBatteryIsLow = status.isBatteryLow();
629            final MutableInt tmpIcon = new MutableInt(0);
630            update(BATTERY_INFO, getAltTextMessage(tmpIcon));
631        }
632
633        @Override
634        public void onTimeChanged() {
635            refreshDate();
636        }
637
638        @Override
639        public void onRefreshCarrierInfo(CharSequence plmn, CharSequence spn) {
640            mPlmn = plmn;
641            mSpn = spn;
642            updateCarrierStateWithSimStatus(mSimState);
643        }
644
645        @Override
646        public void onPhoneStateChanged(int phoneState) {
647            mPhoneState = phoneState;
648            updateEmergencyCallButtonState(phoneState);
649        }
650
651        @Override
652        public void onSimStateChanged(IccCardConstants.State simState) {
653            updateCarrierStateWithSimStatus(simState);
654        }
655    };
656
657    public void onClick(View v) {
658        if (v == mEmergencyCallButton) {
659            mCallback.takeEmergencyCallAction();
660        }
661    }
662
663    /**
664     * Performs concentenation of PLMN/SPN
665     * @param plmn
666     * @param spn
667     * @return
668     */
669    private static CharSequence makeCarierString(CharSequence plmn, CharSequence spn) {
670        final boolean plmnValid = !TextUtils.isEmpty(plmn);
671        final boolean spnValid = !TextUtils.isEmpty(spn);
672        if (plmnValid && spnValid) {
673            return plmn + "|" + spn;
674        } else if (plmnValid) {
675            return plmn;
676        } else if (spnValid) {
677            return spn;
678        } else {
679            return "";
680        }
681    }
682}
683