Snap for 6001391 from 3386e7bd94c533837bd434c23757aa8aa1176b3e to qt-aml-networking-release

Change-Id: I65033beeed631598d5a26bd38b3ecbbb0697ad46
diff --git a/src/java/com/android/internal/telephony/BaseCommands.java b/src/java/com/android/internal/telephony/BaseCommands.java
index 89d5c27..f0c6262 100644
--- a/src/java/com/android/internal/telephony/BaseCommands.java
+++ b/src/java/com/android/internal/telephony/BaseCommands.java
@@ -167,11 +167,9 @@
 
     @Override
     public void registerForRadioStateChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
         synchronized (mStateMonitor) {
-            mRadioStateChangedRegistrants.add(r);
-            r.notifyRegistrant();
+            mRadioStateChangedRegistrants.addUnique(h, what, obj);
+            Message.obtain(h, what, new AsyncResult(obj, null, null)).sendToTarget();
         }
     }
 
@@ -183,8 +181,7 @@
     }
 
     public void registerForImsNetworkStateChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mImsNetworkStateChangedRegistrants.add(r);
+        mImsNetworkStateChangedRegistrants.addUnique(h, what, obj);
     }
 
     public void unregisterForImsNetworkStateChanged(Handler h) {
@@ -193,13 +190,11 @@
 
     @Override
     public void registerForOn(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
         synchronized (mStateMonitor) {
-            mOnRegistrants.add(r);
+            mOnRegistrants.addUnique(h, what, obj);
 
             if (mState == TelephonyManager.RADIO_POWER_ON) {
-                r.notifyRegistrant(new AsyncResult(null, null, null));
+                Message.obtain(h, what, new AsyncResult(obj, null, null)).sendToTarget();
             }
         }
     }
@@ -213,13 +208,11 @@
 
     @Override
     public void registerForAvailable(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
         synchronized (mStateMonitor) {
-            mAvailRegistrants.add(r);
+            mAvailRegistrants.addUnique(h, what, obj);
 
             if (mState != TelephonyManager.RADIO_POWER_UNAVAILABLE) {
-                r.notifyRegistrant(new AsyncResult(null, null, null));
+                Message.obtain(h, what, new AsyncResult(obj, null, null)).sendToTarget();
             }
         }
     }
@@ -233,13 +226,11 @@
 
     @Override
     public void registerForNotAvailable(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
         synchronized (mStateMonitor) {
-            mNotAvailRegistrants.add(r);
+            mNotAvailRegistrants.addUnique(h, what, obj);
 
             if (mState == TelephonyManager.RADIO_POWER_UNAVAILABLE) {
-                r.notifyRegistrant(new AsyncResult(null, null, null));
+                Message.obtain(h, what, new AsyncResult(obj, null, null)).sendToTarget();
             }
         }
     }
@@ -253,14 +244,12 @@
 
     @Override
     public void registerForOffOrNotAvailable(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
         synchronized (mStateMonitor) {
-            mOffOrNotAvailRegistrants.add(r);
+            mOffOrNotAvailRegistrants.addUnique(h, what, obj);
 
             if (mState == TelephonyManager.RADIO_POWER_OFF
                     || mState == TelephonyManager.RADIO_POWER_UNAVAILABLE) {
-                r.notifyRegistrant(new AsyncResult(null, null, null));
+                Message.obtain(h, what, new AsyncResult(obj, null, null)).sendToTarget();
             }
         }
     }
@@ -273,9 +262,7 @@
 
     @Override
     public void registerForCallStateChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
-        mCallStateRegistrants.add(r);
+        mCallStateRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -285,9 +272,7 @@
 
     @Override
     public void registerForNetworkStateChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
-        mNetworkStateRegistrants.add(r);
+        mNetworkStateRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -297,9 +282,7 @@
 
     @Override
     public void registerForDataCallListChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
-        mDataCallListChangedRegistrants.add(r);
+        mDataCallListChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -309,8 +292,7 @@
 
     @Override
     public void registerForVoiceRadioTechChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mVoiceRadioTechChangedRegistrants.add(r);
+        mVoiceRadioTechChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -320,8 +302,7 @@
 
     @Override
     public void registerForIccStatusChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mIccStatusChangedRegistrants.add(r);
+        mIccStatusChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -331,8 +312,7 @@
 
     @Override
     public void registerForIccSlotStatusChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant(h, what, obj);
-        mIccSlotStatusChangedRegistrants.add(r);
+        mIccSlotStatusChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -524,8 +504,7 @@
 
     @Override
     public void registerForIccRefresh(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mIccRefreshRegistrants.add(r);
+        mIccRefreshRegistrants.addUnique(h, what, obj);
     }
     @Override
     public void setOnIccRefresh(Handler h, int what, Object obj) {
@@ -581,8 +560,7 @@
 
     @Override
     public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mVoicePrivacyOnRegistrants.add(r);
+        mVoicePrivacyOnRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -592,8 +570,7 @@
 
     @Override
     public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mVoicePrivacyOffRegistrants.add(r);
+        mVoicePrivacyOffRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -616,8 +593,7 @@
 
     @Override
     public void registerForDisplayInfo(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mDisplayInfoRegistrants.add(r);
+        mDisplayInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -627,8 +603,7 @@
 
     @Override
     public void registerForCallWaitingInfo(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mCallWaitingInfoRegistrants.add(r);
+        mCallWaitingInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -638,8 +613,7 @@
 
     @Override
     public void registerForSignalInfo(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mSignalInfoRegistrants.add(r);
+        mSignalInfoRegistrants.addUnique(h, what, obj);
     }
 
     public void setOnUnsolOemHookRaw(Handler h, int what, Object obj) {
@@ -660,8 +634,7 @@
 
     @Override
     public void registerForCdmaOtaProvision(Handler h,int what, Object obj){
-        Registrant r = new Registrant (h, what, obj);
-        mOtaProvisionRegistrants.add(r);
+        mOtaProvisionRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -671,8 +644,7 @@
 
     @Override
     public void registerForNumberInfo(Handler h,int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mNumberInfoRegistrants.add(r);
+        mNumberInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -682,8 +654,7 @@
 
      @Override
     public void registerForRedirectedNumberInfo(Handler h,int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mRedirNumInfoRegistrants.add(r);
+        mRedirNumInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -693,8 +664,7 @@
 
     @Override
     public void registerForLineControlInfo(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mLineControlInfoRegistrants.add(r);
+        mLineControlInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -704,8 +674,7 @@
 
     @Override
     public void registerFoT53ClirlInfo(Handler h,int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mT53ClirInfoRegistrants.add(r);
+        mT53ClirInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -715,8 +684,7 @@
 
     @Override
     public void registerForT53AudioControlInfo(Handler h,int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mT53AudCntrlInfoRegistrants.add(r);
+        mT53AudCntrlInfoRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -726,8 +694,7 @@
 
     @Override
     public void registerForRingbackTone(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mRingbackToneRegistrants.add(r);
+        mRingbackToneRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -737,8 +704,7 @@
 
     @Override
     public void registerForResendIncallMute(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mResendIncallMuteRegistrants.add(r);
+        mResendIncallMuteRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -748,8 +714,7 @@
 
     @Override
     public void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mCdmaSubscriptionChangedRegistrants.add(r);
+        mCdmaSubscriptionChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -759,8 +724,7 @@
 
     @Override
     public void registerForCdmaPrlChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mCdmaPrlChangedRegistrants.add(r);
+        mCdmaPrlChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -770,8 +734,7 @@
 
     @Override
     public void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mExitEmergencyCallbackModeRegistrants.add(r);
+        mExitEmergencyCallbackModeRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -781,8 +744,7 @@
 
     @Override
     public void registerForHardwareConfigChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mHardwareConfigChangeRegistrants.add(r);
+        mHardwareConfigChangeRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -793,7 +755,7 @@
     @Override
     public void registerForNetworkScanResult(Handler h, int what, Object obj) {
         Registrant r = new Registrant(h, what, obj);
-        mRilNetworkScanResultRegistrants.add(r);
+        mRilNetworkScanResultRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -806,10 +768,10 @@
      */
     @Override
     public void registerForRilConnected(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mRilConnectedRegistrants.add(r);
+        mRilConnectedRegistrants.addUnique(h, what, obj);
         if (mRilVersion != -1) {
-            r.notifyRegistrant(new AsyncResult(null, new Integer(mRilVersion), null));
+            Message.obtain(h, what, new AsyncResult(obj, new Integer(mRilVersion), null))
+                    .sendToTarget();
         }
     }
 
@@ -819,8 +781,7 @@
     }
 
     public void registerForSubscriptionStatusChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mSubscriptionStatusRegistrants.add(r);
+        mSubscriptionStatusRegistrants.addUnique(h, what, obj);
     }
 
     public void unregisterForSubscriptionStatusChanged(Handler h) {
@@ -829,8 +790,7 @@
 
     @Override
     public void registerForEmergencyNumberList(Handler h, int what, Object obj) {
-        Registrant r = new Registrant(h, what, obj);
-        mEmergencyNumberListRegistrants.add(r);
+        mEmergencyNumberListRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -901,8 +861,7 @@
      */
     @Override
     public void registerForCellInfoList(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-        mRilCellInfoListRegistrants.add(r);
+        mRilCellInfoListRegistrants.addUnique(h, what, obj);
     }
     @Override
     public void unregisterForCellInfoList(Handler h) {
@@ -911,8 +870,7 @@
 
     @Override
     public void registerForPhysicalChannelConfiguration(Handler h, int what, Object obj) {
-        Registrant r = new Registrant(h, what, obj);
-        mPhysicalChannelConfigurationRegistrants.add(r);
+        mPhysicalChannelConfigurationRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -922,9 +880,7 @@
 
     @Override
     public void registerForSrvccStateChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant (h, what, obj);
-
-        mSrvccStateRegistrants.add(r);
+        mSrvccStateRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -961,8 +917,7 @@
 
     @Override
     public void registerForRadioCapabilityChanged(Handler h, int what, Object obj) {
-        Registrant r = new Registrant(h, what, obj);
-        mPhoneRadioCapabilityChangedRegistrants.add(r);
+        mPhoneRadioCapabilityChangedRegistrants.addUnique(h, what, obj);
     }
 
     @Override
@@ -984,10 +939,8 @@
 
     @Override
     public void registerForLceInfo(Handler h, int what, Object obj) {
-        Registrant r = new Registrant(h, what, obj);
-
         synchronized (mStateMonitor) {
-            mLceInfoRegistrants.add(r);
+            mLceInfoRegistrants.addUnique(h, what, obj);
         }
     }
 
@@ -1033,7 +986,7 @@
         Registrant r = new Registrant(h, what, obj);
 
         synchronized (mStateMonitor) {
-            mNattKeepaliveStatusRegistrants.add(r);
+            mNattKeepaliveStatusRegistrants.addUnique(h, what, obj);
         }
     }
 
diff --git a/src/java/com/android/internal/telephony/BtSmsInterfaceManager.java b/src/java/com/android/internal/telephony/BtSmsInterfaceManager.java
index 408d7ab..67e07bc 100644
--- a/src/java/com/android/internal/telephony/BtSmsInterfaceManager.java
+++ b/src/java/com/android/internal/telephony/BtSmsInterfaceManager.java
@@ -45,13 +45,13 @@
         BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
         if (btAdapter == null) {
             // No bluetooth service on this platform?
-            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_ERROR_NO_SERVICE);
+            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_NO_BLUETOOTH_SERVICE);
             return;
         }
         BluetoothDevice device = btAdapter.getRemoteDevice(info.getIccId());
         if (device == null) {
             Log.d(LOG_TAG, "Bluetooth device addr invalid: " + info.getIccId());
-            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_ERROR_NO_SERVICE);
+            sendErrorInPendingIntent(sentIntent, SmsManager.RESULT_INVALID_BLUETOOTH_ADDRESS);
             return;
         }
         btAdapter.getProfileProxy(ActivityThread.currentApplication().getApplicationContext(),
@@ -113,7 +113,7 @@
         public void onServiceDisconnected(int profile) {
             if (mMessage != null) {
                 Log.d(LOG_TAG, "Bluetooth disconnected before sending the message");
-                sendErrorInPendingIntent(mSentIntent, SmsManager.RESULT_ERROR_NO_SERVICE);
+                sendErrorInPendingIntent(mSentIntent, SmsManager.RESULT_BLUETOOTH_DISCONNECTED);
                 mMessage = null;
             }
         }
diff --git a/src/java/com/android/internal/telephony/Call.java b/src/java/com/android/internal/telephony/Call.java
index 37fefd9..cce5ee7 100644
--- a/src/java/com/android/internal/telephony/Call.java
+++ b/src/java/com/android/internal/telephony/Call.java
@@ -16,25 +16,43 @@
 
 package com.android.internal.telephony;
 
+import android.annotation.UnsupportedAppUsage;
 import android.telecom.ConferenceParticipant;
+import android.telephony.Rlog;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import android.annotation.UnsupportedAppUsage;
-import android.telephony.Rlog;
-
 /**
  * {@hide}
  */
 public abstract class Call {
     protected final String LOG_TAG = "Call";
 
-    /* Enums */
+    @UnsupportedAppUsage
+    public Call() {
+    }
 
+    /* Enums */
+    @UnsupportedAppUsage(implicitMember = "values()[Lcom/android/internal/telephony/Call$State;")
     public enum State {
         @UnsupportedAppUsage
-        IDLE, ACTIVE, HOLDING, DIALING, ALERTING, INCOMING, WAITING, DISCONNECTED, DISCONNECTING;
+        IDLE,
+        ACTIVE,
+        @UnsupportedAppUsage
+        HOLDING,
+        @UnsupportedAppUsage
+        DIALING,
+        @UnsupportedAppUsage
+        ALERTING,
+        @UnsupportedAppUsage
+        INCOMING,
+        @UnsupportedAppUsage
+        WAITING,
+        @UnsupportedAppUsage
+        DISCONNECTED,
+        @UnsupportedAppUsage
+        DISCONNECTING;
 
         @UnsupportedAppUsage
         public boolean isAlive() {
diff --git a/src/java/com/android/internal/telephony/CallForwardInfo.java b/src/java/com/android/internal/telephony/CallForwardInfo.java
index ea3ba27..d9967ad 100644
--- a/src/java/com/android/internal/telephony/CallForwardInfo.java
+++ b/src/java/com/android/internal/telephony/CallForwardInfo.java
@@ -28,6 +28,10 @@
     private static final String TAG = "CallForwardInfo";
 
     @UnsupportedAppUsage
+    public CallForwardInfo() {
+    }
+
+    @UnsupportedAppUsage
     public int             status;      /*1 = active, 0 = not active */
     @UnsupportedAppUsage
     public int             reason;      /* from TS 27.007 7.11 "reason" */
diff --git a/src/java/com/android/internal/telephony/CallTracker.java b/src/java/com/android/internal/telephony/CallTracker.java
index 002e082..f24b3c8 100644
--- a/src/java/com/android/internal/telephony/CallTracker.java
+++ b/src/java/com/android/internal/telephony/CallTracker.java
@@ -75,6 +75,10 @@
     protected static final int EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA = 16;
     protected static final int EVENT_THREE_WAY_DIAL_BLANK_FLASH    = 20;
 
+    @UnsupportedAppUsage
+    public CallTracker() {
+    }
+
     protected void pollCallsWhenSafe() {
         mNeedsPoll = true;
 
diff --git a/src/java/com/android/internal/telephony/CarrierServiceBindHelper.java b/src/java/com/android/internal/telephony/CarrierServiceBindHelper.java
index 59bd9b4..0393ffa 100644
--- a/src/java/com/android/internal/telephony/CarrierServiceBindHelper.java
+++ b/src/java/com/android/internal/telephony/CarrierServiceBindHelper.java
@@ -23,6 +23,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
+import android.content.pm.ComponentInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.os.Bundle;
@@ -42,6 +43,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 
+import com.android.internal.telephony.util.TelephonyUtils;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.List;
@@ -228,8 +230,9 @@
             String candidateServiceClass = null;
             if (carrierResolveInfo != null) {
                 metadata = carrierResolveInfo.serviceInfo.metaData;
-                candidateServiceClass =
-                        carrierResolveInfo.getComponentInfo().getComponentName().getClassName();
+                ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(carrierResolveInfo);
+                candidateServiceClass = new ComponentName(componentInfo.packageName,
+                    componentInfo.name).getClassName();
             }
 
             // Only bind if the service wants it
diff --git a/src/java/com/android/internal/telephony/CellBroadcastServiceManager.java b/src/java/com/android/internal/telephony/CellBroadcastServiceManager.java
index 2e82747..132ea3b 100644
--- a/src/java/com/android/internal/telephony/CellBroadcastServiceManager.java
+++ b/src/java/com/android/internal/telephony/CellBroadcastServiceManager.java
@@ -25,11 +25,13 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
+import android.os.RemoteCallback;
 import android.os.RemoteException;
 import android.telephony.CellBroadcastService;
 import android.telephony.ICellBroadcastService;
 import android.util.LocalLog;
 import android.util.Log;
+import android.util.Pair;
 
 import com.android.internal.telephony.cdma.SmsMessage;
 
@@ -57,6 +59,7 @@
     /** New SMS cell broadcast received as an AsyncResult. */
     private static final int EVENT_NEW_GSM_SMS_CB = 0;
     private static final int EVENT_NEW_CDMA_SMS_CB = 1;
+    private static final int EVENT_NEW_CDMA_SCP_MESSAGE = 2;
     private boolean mEnabled;
 
     public CellBroadcastServiceManager(Context context, Phone phone) {
@@ -66,7 +69,7 @@
     }
 
     /**
-     * Send a GSM CB message to the CellBroadcastServieManager's handler.
+     * Send a GSM CB message to the CellBroadcastServiceManager's handler.
      * @param m the message
      */
     public void sendGsmMessageToHandler(Message m) {
@@ -75,7 +78,7 @@
     }
 
     /**
-     * Send a CDMA CB message to the CellBroadcastServieManager's handler.
+     * Send a CDMA CB message to the CellBroadcastServiceManager's handler.
      * @param sms the SmsMessage to forward
      */
     public void sendCdmaMessageToHandler(SmsMessage sms) {
@@ -86,6 +89,17 @@
     }
 
     /**
+     * Send a CDMA Service Category Program message to the CellBroadcastServiceManager's handler.
+     * @param sms the SCP message
+     */
+    public void sendCdmaScpMessageToHandler(SmsMessage sms, RemoteCallback callback) {
+        Message m = Message.obtain();
+        m.what = EVENT_NEW_CDMA_SCP_MESSAGE;
+        m.obj = Pair.create(sms, callback);
+        mModuleCellBroadcastHandler.sendMessage(m);
+    }
+
+    /**
      * Enable the CB module. The CellBroadcastService will be bound to and CB messages from the
      * RIL will be forwarded to the module.
      */
@@ -99,7 +113,9 @@
     public void disable() {
         mEnabled = false;
         mPhone.mCi.unSetOnNewGsmBroadcastSms(mModuleCellBroadcastHandler);
-        mContext.unbindService(sServiceConnection);
+        if (sServiceConnection.mService != null) {
+            mContext.unbindService(sServiceConnection);
+        }
     }
 
     /**
@@ -138,7 +154,16 @@
                             SmsMessage sms = (SmsMessage) msg.obj;
                             cellBroadcastService.handleCdmaCellBroadcastSms(mPhone.getPhoneId(),
                                     sms.getEnvelopeBearerData(), sms.getEnvelopeServiceCategory());
-
+                        } else if (msg.what == EVENT_NEW_CDMA_SCP_MESSAGE) {
+                            mLocalLog.log("CDMA SCP message for phone " + mPhone.getPhoneId());
+                            Pair<SmsMessage, RemoteCallback> smsAndCallback =
+                                    (Pair<SmsMessage, RemoteCallback>) msg.obj;
+                            SmsMessage sms = smsAndCallback.first;
+                            RemoteCallback callback = smsAndCallback.second;
+                            cellBroadcastService.handleCdmaScpMessage(mPhone.getPhoneId(),
+                                    sms.getSmsCbProgramData(),
+                                    sms.getOriginatingAddress(),
+                                    callback);
                         }
                     } catch (RemoteException e) {
                         Log.e(TAG, "Failed to connect to default app: "
@@ -154,7 +179,16 @@
             Intent intent = new Intent(CellBroadcastService.CELL_BROADCAST_SERVICE_INTERFACE);
             intent.setPackage(mCellBroadcastServicePackage);
             if (sServiceConnection.mService == null) {
-                mContext.bindService(intent, sServiceConnection, Context.BIND_AUTO_CREATE);
+                boolean serviceWasBound = mContext.bindService(intent, sServiceConnection,
+                        Context.BIND_AUTO_CREATE);
+                Log.d(TAG, "serviceWasBound=" + serviceWasBound);
+                if (!serviceWasBound) {
+                    Log.e(TAG, "Unable to bind to service");
+                    mLocalLog.log("Unable to bind to service");
+                    return;
+                }
+            } else {
+                Log.d(TAG, "skipping bindService because connection already exists");
             }
             mPhone.mCi.setOnNewGsmBroadcastSms(mModuleCellBroadcastHandler, EVENT_NEW_GSM_SMS_CB,
                     null);
diff --git a/src/java/com/android/internal/telephony/CommandException.java b/src/java/com/android/internal/telephony/CommandException.java
index 4642d90..31ffcd2 100644
--- a/src/java/com/android/internal/telephony/CommandException.java
+++ b/src/java/com/android/internal/telephony/CommandException.java
@@ -123,6 +123,7 @@
         OEM_ERROR_23,
         OEM_ERROR_24,
         OEM_ERROR_25,
+        REQUEST_CANCELLED,
     }
 
     @UnsupportedAppUsage
@@ -321,6 +322,8 @@
                 return new CommandException(Error.OEM_ERROR_24);
             case RILConstants.OEM_ERROR_25:
                 return new CommandException(Error.OEM_ERROR_25);
+            case RILConstants.REQUEST_CANCELLED:
+                return new CommandException(Error.REQUEST_CANCELLED);
 
             default:
                 Rlog.e("GSM", "Unrecognized RIL errno " + ril_errno);
diff --git a/src/java/com/android/internal/telephony/DriverCall.java b/src/java/com/android/internal/telephony/DriverCall.java
index 8c2c3db..3a7947d 100644
--- a/src/java/com/android/internal/telephony/DriverCall.java
+++ b/src/java/com/android/internal/telephony/DriverCall.java
@@ -17,9 +17,8 @@
 package com.android.internal.telephony;
 
 import android.annotation.UnsupportedAppUsage;
-import android.telephony.Rlog;
-import java.lang.Comparable;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
 
 /**
  * {@hide}
@@ -27,6 +26,8 @@
 public class DriverCall implements Comparable<DriverCall> {
     static final String LOG_TAG = "DriverCall";
 
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/DriverCall$State;")
     public enum State {
         @UnsupportedAppUsage
         ACTIVE,
diff --git a/src/java/com/android/internal/telephony/IIccPhoneBook.aidl b/src/java/com/android/internal/telephony/IIccPhoneBook.aidl
index fbbdee2..dc990de 100644
--- a/src/java/com/android/internal/telephony/IIccPhoneBook.aidl
+++ b/src/java/com/android/internal/telephony/IIccPhoneBook.aidl
@@ -30,6 +30,7 @@
      * @param efid the EF id of a ADN-like SIM
      * @return List of AdnRecord
      */
+    @UnsupportedAppUsage
     List<AdnRecord> getAdnRecordsInEf(int efid);
 
     /**
@@ -40,6 +41,7 @@
      * @param subId user preferred subId
      * @return List of AdnRecord
      */
+    @UnsupportedAppUsage
     List<AdnRecord> getAdnRecordsInEfForSubscriber(int subId, int efid);
 
     /**
@@ -60,6 +62,7 @@
      * @param pin2 required to update EF_FDN, otherwise must be null
      * @return true for success
      */
+    @UnsupportedAppUsage
     boolean updateAdnRecordsInEfBySearch(int efid,
             String oldTag, String oldPhoneNumber,
             String newTag, String newPhoneNumber,
@@ -138,6 +141,7 @@
      *            recordSizes[1]  is the total length of the EF file
      *            recordSizes[2]  is the number of records in the EF file
      */
+    @UnsupportedAppUsage
     int[] getAdnRecordsSize(int efid);
 
     /**
@@ -150,6 +154,7 @@
      *            recordSizes[1]  is the total length of the EF file
      *            recordSizes[2]  is the number of records in the EF file
      */
+    @UnsupportedAppUsage
     int[] getAdnRecordsSizeForSubscriber(int subId, int efid);
 
 }
diff --git a/src/java/com/android/internal/telephony/IccProvider.java b/src/java/com/android/internal/telephony/IccProvider.java
index 3ac4027..ae5cd7b 100644
--- a/src/java/com/android/internal/telephony/IccProvider.java
+++ b/src/java/com/android/internal/telephony/IccProvider.java
@@ -81,6 +81,10 @@
 
     private SubscriptionManager mSubscriptionManager;
 
+    @UnsupportedAppUsage
+    public IccProvider() {
+    }
+
     @Override
     public boolean onCreate() {
         mSubscriptionManager = SubscriptionManager.from(getContext());
diff --git a/src/java/com/android/internal/telephony/MultiSimSettingController.java b/src/java/com/android/internal/telephony/MultiSimSettingController.java
index 5ae88f8..1ce360f 100644
--- a/src/java/com/android/internal/telephony/MultiSimSettingController.java
+++ b/src/java/com/android/internal/telephony/MultiSimSettingController.java
@@ -36,6 +36,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.Message;
 import android.os.ParcelUuid;
@@ -79,6 +80,7 @@
     private static final int EVENT_SUBSCRIPTION_GROUP_CHANGED        = 5;
     private static final int EVENT_DEFAULT_DATA_SUBSCRIPTION_CHANGED = 6;
     private static final int EVENT_CARRIER_CONFIG_CHANGED            = 7;
+    private static final int EVENT_MULTI_SIM_CONFIG_CHANGED          = 8;
 
     @Retention(RetentionPolicy.SOURCE)
     @IntDef(prefix = {"PRIMARY_SUB_"},
@@ -188,6 +190,9 @@
         mCarrierConfigLoadedSubIds = new int[phoneCount];
         Arrays.fill(mCarrierConfigLoadedSubIds, SubscriptionManager.INVALID_SUBSCRIPTION_ID);
 
+        PhoneConfigurationManager.registerForMultiSimConfigChange(
+                this, EVENT_MULTI_SIM_CONFIG_CHANGED, null);
+
         context.registerReceiver(mIntentReceiver, new IntentFilter(
                 CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
     }
@@ -268,6 +273,9 @@
                 int subId = msg.arg2;
                 onCarrierConfigChanged(phoneId, subId);
                 break;
+            case EVENT_MULTI_SIM_CONFIG_CHANGED:
+                int activeModems = (int) ((AsyncResult) msg.obj).result;
+                onMultiSimConfigChanged(activeModems);
         }
     }
 
@@ -358,6 +366,14 @@
         return true;
     }
 
+    private void onMultiSimConfigChanged(int activeModems) {
+        // Clear mCarrierConfigLoadedSubIds. Other actions will responds to active
+        // subscription change.
+        for (int phoneId = activeModems; phoneId < mCarrierConfigLoadedSubIds.length; phoneId++) {
+            mCarrierConfigLoadedSubIds[phoneId] = INVALID_SUBSCRIPTION_ID;
+        }
+    }
+
     /**
      * Wait for subInfo initialization (after boot up) and carrier config load for all active
      * subscriptions before re-evaluate multi SIM settings.
diff --git a/src/java/com/android/internal/telephony/NetworkScanRequestTracker.java b/src/java/com/android/internal/telephony/NetworkScanRequestTracker.java
index ba1081b..c848358 100644
--- a/src/java/com/android/internal/telephony/NetworkScanRequestTracker.java
+++ b/src/java/com/android/internal/telephony/NetworkScanRequestTracker.java
@@ -525,12 +525,12 @@
         // stopped, a new scan will automatically start with nsri.
         // The new scan can interrupt the live scan only when all the below requirements are met:
         //   1. There is 1 live scan and no other pending scan
-        //   2. The new scan is requested by mobile network setting menu (owned by PHONE process)
+        //   2. The new scan is requested by mobile network setting menu (owned by SYSTEM process)
         //   3. The live scan is not requested by mobile network setting menu
         private synchronized boolean interruptLiveScan(NetworkScanRequestInfo nsri) {
             if (mLiveRequestInfo != null && mPendingRequestInfo == null
-                    && nsri.mUid == Process.PHONE_UID
-                            && mLiveRequestInfo.mUid != Process.PHONE_UID) {
+                    && nsri.mUid == Process.SYSTEM_UID
+                            && mLiveRequestInfo.mUid != Process.SYSTEM_UID) {
                 doInterruptScan(mLiveRequestInfo.mScanId);
                 mPendingRequestInfo = nsri;
                 notifyMessenger(mLiveRequestInfo, TelephonyScanManager.CALLBACK_SCAN_ERROR,
diff --git a/src/java/com/android/internal/telephony/ProxyController.java b/src/java/com/android/internal/telephony/ProxyController.java
index a3b5c04..54fcac9 100644
--- a/src/java/com/android/internal/telephony/ProxyController.java
+++ b/src/java/com/android/internal/telephony/ProxyController.java
@@ -26,6 +26,7 @@
 import android.os.PowerManager.WakeLock;
 import android.telephony.RadioAccessFamily;
 import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.Log;
 
@@ -151,7 +152,7 @@
     public void registerForAllDataDisconnected(int subId, Handler h, int what) {
         int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
 
-        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getSupportedModemCount()) {
+        if (SubscriptionManager.isValidPhoneId(phoneId)) {
             mPhones[phoneId].registerForAllDataDisconnected(h, what);
         }
     }
@@ -159,7 +160,7 @@
     public void unregisterForAllDataDisconnected(int subId, Handler h) {
         int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
 
-        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getSupportedModemCount()) {
+        if (SubscriptionManager.isValidPhoneId(phoneId)) {
             mPhones[phoneId].unregisterForAllDataDisconnected(h);
         }
     }
@@ -168,7 +169,7 @@
     public boolean areAllDataDisconnected(int subId) {
         int phoneId = SubscriptionController.getInstance().getPhoneId(subId);
 
-        if (phoneId >= 0 && phoneId < TelephonyManager.getDefault().getSupportedModemCount()) {
+        if (SubscriptionManager.isValidPhoneId(phoneId)) {
             return mPhones[phoneId].areAllDataDisconnected();
         } else {
             // if we can't find a phone for the given subId, it is disconnected.
diff --git a/src/java/com/android/internal/telephony/RIL.java b/src/java/com/android/internal/telephony/RIL.java
index dd4bdba..4d7a687 100644
--- a/src/java/com/android/internal/telephony/RIL.java
+++ b/src/java/com/android/internal/telephony/RIL.java
@@ -393,7 +393,7 @@
         }
     }
 
-    private void resetProxyAndRequestList() {
+    private synchronized void resetProxyAndRequestList() {
         mRadioProxy = null;
         mOemHookProxy = null;
 
diff --git a/src/java/com/android/internal/telephony/SMSDispatcher.java b/src/java/com/android/internal/telephony/SMSDispatcher.java
index 8c1e0e8..0f3d8aa 100644
--- a/src/java/com/android/internal/telephony/SMSDispatcher.java
+++ b/src/java/com/android/internal/telephony/SMSDispatcher.java
@@ -17,7 +17,6 @@
 package com.android.internal.telephony;
 
 import static android.Manifest.permission.SEND_SMS_NO_CONFIRMATION;
-import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE;
 import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE;
 import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED;
 import static android.telephony.SmsManager.RESULT_ERROR_NONE;
@@ -340,8 +339,8 @@
                 Rlog.d(TAG, "SMSDispatcher: EVENT_STOP_SENDING - "
                         + "sending LIMIT_EXCEEDED error code.");
             } else {
-                error = RESULT_ERROR_GENERIC_FAILURE;
-                Rlog.e(TAG, "SMSDispatcher: EVENT_STOP_SENDING - unexpected cases.");
+                    error = SmsManager.RESULT_UNEXPECTED_EVENT_STOP_SENDING;
+                    Rlog.e(TAG, "SMSDispatcher: EVENT_STOP_SENDING - unexpected cases.");
             }
 
             handleSmsTrackersFailure(trackers, error, NO_ERROR_CODE);
@@ -649,7 +648,8 @@
     private void sendSubmitPdu(SmsTracker[] trackers) {
         if (shouldBlockSmsForEcbm()) {
             Rlog.d(TAG, "Block SMS in Emergency Callback mode");
-            handleSmsTrackersFailure(trackers, RESULT_ERROR_NO_SERVICE, NO_ERROR_CODE);
+            handleSmsTrackersFailure(trackers, SmsManager.RESULT_SMS_BLOCKED_DURING_EMERGENCY,
+                    NO_ERROR_CODE);
         } else {
             sendRawPdu(trackers);
         }
@@ -729,16 +729,62 @@
                 if (ar.result != null) {
                     errorCode = ((SmsResponse)ar.result).mErrorCode;
                 }
-                int error = RESULT_ERROR_GENERIC_FAILURE;
-                if (((CommandException)(ar.exception)).getCommandError()
-                        == CommandException.Error.FDN_CHECK_FAILURE) {
-                    error = RESULT_ERROR_FDN_CHECK_FAILURE;
-                }
+                int error = rilErrorToSmsManagerResult(((CommandException) (ar.exception))
+                        .getCommandError());
                 tracker.onFailed(mContext, error, errorCode);
             }
         }
     }
 
+    private static int rilErrorToSmsManagerResult(CommandException.Error rilError) {
+        switch (rilError) {
+            case RADIO_NOT_AVAILABLE:
+                return SmsManager.RESULT_RIL_RADIO_NOT_AVAILABLE;
+            case SMS_FAIL_RETRY:
+                return SmsManager.RESULT_RIL_SMS_SEND_FAIL_RETRY;
+            case NETWORK_REJECT:
+                return SmsManager.RESULT_RIL_NETWORK_REJECT;
+            case INVALID_STATE:
+                return SmsManager.RESULT_RIL_INVALID_STATE;
+            case INVALID_ARGUMENTS:
+                return SmsManager.RESULT_RIL_INVALID_ARGUMENTS;
+            case NO_MEMORY:
+                return SmsManager.RESULT_RIL_NO_MEMORY;
+            case REQUEST_RATE_LIMITED:
+                return SmsManager.RESULT_RIL_REQUEST_RATE_LIMITED;
+            case INVALID_SMS_FORMAT:
+                return SmsManager.RESULT_RIL_INVALID_SMS_FORMAT;
+            case SYSTEM_ERR:
+                return SmsManager.RESULT_RIL_SYSTEM_ERR;
+            case ENCODING_ERR:
+                return SmsManager.RESULT_RIL_ENCODING_ERR;
+            case MODEM_ERR:
+                return SmsManager.RESULT_RIL_MODEM_ERR;
+            case NETWORK_ERR:
+                return SmsManager.RESULT_RIL_NETWORK_ERR;
+            case INTERNAL_ERR:
+                return SmsManager.RESULT_RIL_INTERNAL_ERR;
+            case REQUEST_NOT_SUPPORTED:
+                return SmsManager.RESULT_RIL_REQUEST_NOT_SUPPORTED;
+            case INVALID_MODEM_STATE:
+                return SmsManager.RESULT_RIL_INVALID_MODEM_STATE;
+            case NETWORK_NOT_READY:
+                return SmsManager.RESULT_RIL_NETWORK_NOT_READY;
+            case OPERATION_NOT_ALLOWED:
+                return SmsManager.RESULT_RIL_OPERATION_NOT_ALLOWED;
+            case NO_RESOURCES:
+                return SmsManager.RESULT_RIL_NO_RESOURCES;
+            case REQUEST_CANCELLED:
+                return SmsManager.RESULT_RIL_CANCELLED;
+            case SIM_ABSENT:
+                return SmsManager.RESULT_RIL_SIM_ABSENT;
+            case FDN_CHECK_FAILURE:
+                return SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE;
+            default:
+                return RESULT_ERROR_GENERIC_FAILURE;
+        }
+    }
+
     /**
      * Handles outbound message when the phone is not in service.
      *
diff --git a/src/java/com/android/internal/telephony/ServiceStateTracker.java b/src/java/com/android/internal/telephony/ServiceStateTracker.java
index 32b0cff..561a051 100755
--- a/src/java/com/android/internal/telephony/ServiceStateTracker.java
+++ b/src/java/com/android/internal/telephony/ServiceStateTracker.java
@@ -1570,10 +1570,11 @@
                         mNrStateChangedRegistrants.notifyRegistrants();
                         hasChanged = true;
                     }
+                    hasChanged |= RatRatcheter
+                            .updateBandwidths(getBandwidthsFromConfigs(list), mSS);
 
                     // Notify NR frequency, NR connection status or bandwidths changed.
-                    if (hasChanged
-                            || RatRatcheter.updateBandwidths(getBandwidthsFromConfigs(list), mSS)) {
+                    if (hasChanged) {
                         mPhone.notifyServiceStateChanged(mSS);
                     }
                 }
diff --git a/src/java/com/android/internal/telephony/SmsDispatchersController.java b/src/java/com/android/internal/telephony/SmsDispatchersController.java
index c75b478..6b7c8a3 100644
--- a/src/java/com/android/internal/telephony/SmsDispatchersController.java
+++ b/src/java/com/android/internal/telephony/SmsDispatchersController.java
@@ -462,7 +462,7 @@
                 || (map.containsKey("data") && map.containsKey("destPort"))))) {
             // should never come here...
             Rlog.e(TAG, "sendRetrySms failed to re-encode per missing fields!");
-            tracker.onFailed(mContext, SmsManager.RESULT_ERROR_GENERIC_FAILURE, NO_ERROR_CODE);
+            tracker.onFailed(mContext, SmsManager.RESULT_SMS_SEND_RETRY_FAILED, NO_ERROR_CODE);
             return;
         }
         String scAddr = (String) map.get("scAddr");
diff --git a/src/java/com/android/internal/telephony/SubscriptionController.java b/src/java/com/android/internal/telephony/SubscriptionController.java
index 34ea10d..24bb42d 100644
--- a/src/java/com/android/internal/telephony/SubscriptionController.java
+++ b/src/java/com/android/internal/telephony/SubscriptionController.java
@@ -61,6 +61,7 @@
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.telephony.uicc.UiccCard;
 import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.util.ArrayUtils;
 
 import java.io.FileDescriptor;
@@ -1332,17 +1333,12 @@
         if (DBG) logdl("[clearSubInfoRecord]+ iccId:" + " slotIndex:" + slotIndex);
 
         // update simInfo db with invalid slot index
-        List<SubscriptionInfo> oldSubInfo = getSubInfoUsingSlotIndexPrivileged(slotIndex);
         ContentResolver resolver = mContext.getContentResolver();
         ContentValues value = new ContentValues(1);
-        value.put(SubscriptionManager.SIM_SLOT_INDEX,
-                SubscriptionManager.INVALID_SIM_SLOT_INDEX);
-        if (oldSubInfo != null) {
-            for (int i = 0; i < oldSubInfo.size(); i++) {
-                resolver.update(SubscriptionManager.getUriForSubscriptionId(
-                        oldSubInfo.get(i).getSubscriptionId()), value, null, null);
-            }
-        }
+        value.put(SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+        String where = "(" + SubscriptionManager.SIM_SLOT_INDEX + "=" + slotIndex + ")";
+        resolver.update(SubscriptionManager.CONTENT_URI, value, where, null);
+
         // Refresh the Cache of Active Subscription Info List
         refreshCachedActiveSubscriptionInfoList();
 
@@ -3743,7 +3739,7 @@
      */
     @NonNull
     public String getDataEnabledOverrideRules(int subId) {
-        return TextUtils.emptyIfNull(getSubscriptionProperty(subId,
+        return TelephonyUtils.emptyIfNull(getSubscriptionProperty(subId,
                 SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES));
     }
 
diff --git a/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java b/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
index 749e800..f20870a 100644
--- a/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
+++ b/src/java/com/android/internal/telephony/SubscriptionInfoUpdater.java
@@ -75,7 +75,7 @@
 public class SubscriptionInfoUpdater extends Handler {
     private static final String LOG_TAG = "SubscriptionInfoUpdater";
     @UnsupportedAppUsage
-    private static final int PROJECT_SIM_NUM = TelephonyManager.getDefault()
+    private static final int SUPPORTED_MODEM_COUNT = TelephonyManager.getDefault()
             .getSupportedModemCount();
 
     private static final boolean DBG = true;
@@ -92,6 +92,7 @@
     private static final int EVENT_SIM_READY = 10;
     private static final int EVENT_SIM_IMSI = 11;
     private static final int EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS = 12;
+    private static final int EVENT_MULTI_SIM_CONFIG_CHANGED = 13;
 
     private static final String ICCID_STRING_FOR_NO_SIM = "";
 
@@ -105,9 +106,9 @@
     private static Context sContext = null;
     @UnsupportedAppUsage
 
-    private static String[] sIccId = new String[PROJECT_SIM_NUM];
-    private static int[] sSimCardState = new int[PROJECT_SIM_NUM];
-    private static int[] sSimApplicationState = new int[PROJECT_SIM_NUM];
+    private static String[] sIccId = new String[SUPPORTED_MODEM_COUNT];
+    private static int[] sSimCardState = new int[SUPPORTED_MODEM_COUNT];
+    private static int[] sSimApplicationState = new int[SUPPORTED_MODEM_COUNT];
     private static boolean sIsSubInfoInitialized = false;
     private SubscriptionManager mSubscriptionManager = null;
     private EuiccManager mEuiccManager;
@@ -153,6 +154,9 @@
 
         mCarrierServiceBindHelper = new CarrierServiceBindHelper(sContext);
         initializeCarrierApps();
+
+        PhoneConfigurationManager.registerForMultiSimConfigChange(
+                this, EVENT_MULTI_SIM_CONFIG_CHANGED, null);
     }
 
     private void initializeCarrierApps() {
@@ -222,7 +226,7 @@
 
     @UnsupportedAppUsage
     private boolean isAllIccIdQueryDone() {
-        for (int i = 0; i < TelephonyManager.getDefault().getPhoneCount(); i++) {
+        for (int i = 0; i < TelephonyManager.getDefault().getActiveModemCount(); i++) {
             UiccSlot slot = UiccController.getInstance().getUiccSlotForPhone(i);
             int slotId = UiccController.getInstance().getSlotIdFromPhoneId(i);
             if  (sIccId[i] == null || slot == null || !slot.isActive()) {
@@ -334,11 +338,25 @@
                 });
                 break;
 
+            case EVENT_MULTI_SIM_CONFIG_CHANGED:
+                onMultiSimConfigChanged();
             default:
                 logd("Unknown msg:" + msg.what);
         }
     }
 
+    private void onMultiSimConfigChanged() {
+        int activeModemCount = ((TelephonyManager) sContext.getSystemService(
+                Context.TELEPHONY_SERVICE)).getActiveModemCount();
+        // For inactive modems, reset its states.
+        for (int phoneId = activeModemCount; phoneId < SUPPORTED_MODEM_COUNT; phoneId++) {
+            SubscriptionController.getInstance().clearSubInfoRecord(phoneId);
+            sIccId[phoneId] = null;
+            sSimCardState[phoneId] = TelephonyManager.SIM_STATE_UNKNOWN;
+            sSimApplicationState[phoneId] = TelephonyManager.SIM_STATE_UNKNOWN;
+        }
+    }
+
     private int getCardIdFromPhoneId(int phoneId) {
         UiccController uiccController = UiccController.getInstance();
         UiccCard card = uiccController.getUiccCardForPhone(phoneId);
diff --git a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
index a461324..d89ee94 100644
--- a/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
+++ b/src/java/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -42,6 +42,7 @@
 import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
 import com.android.internal.telephony.imsphone.ImsPhone;
 import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
+import com.android.internal.telephony.nitz.NewNitzStateMachineImpl;
 import com.android.internal.telephony.uicc.IccCardStatus;
 import com.android.internal.telephony.uicc.UiccCard;
 import com.android.internal.telephony.uicc.UiccProfile;
@@ -292,11 +293,17 @@
         return new EmergencyNumberTracker(phone, ci);
     }
 
+    private static final boolean USE_NEW_NITZ_STATE_MACHINE = false;
+
     /**
      * Returns a new {@link NitzStateMachine} instance.
      */
     public NitzStateMachine makeNitzStateMachine(GsmCdmaPhone phone) {
-        return new NitzStateMachineImpl(phone);
+        if (USE_NEW_NITZ_STATE_MACHINE) {
+            return NewNitzStateMachineImpl.createInstance(phone);
+        } else {
+            return new NitzStateMachineImpl(phone);
+        }
     }
 
     public SimActivationTracker makeSimActivationTracker(Phone phone) {
diff --git a/src/java/com/android/internal/telephony/cat/AppInterface.java b/src/java/com/android/internal/telephony/cat/AppInterface.java
index ed7c822..4f6ca86 100755
--- a/src/java/com/android/internal/telephony/cat/AppInterface.java
+++ b/src/java/com/android/internal/telephony/cat/AppInterface.java
@@ -62,13 +62,20 @@
      */
     void onCmdResponse(CatResponseMessage resMsg);
 
+    /**
+     * Dispose when the service is not longer needed.
+     */
+    void dispose();
+
     /*
      * Enumeration for representing "Type of Command" of proactive commands.
      * Those are the only commands which are supported by the Telephony. Any app
      * implementation should support those.
      * Refer to ETSI TS 102.223 section 9.4
      */
-    public static enum CommandType {
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/cat/AppInterface$CommandType;")
+    enum CommandType {
         @UnsupportedAppUsage
         DISPLAY_TEXT(0x21),
         @UnsupportedAppUsage
diff --git a/src/java/com/android/internal/telephony/cat/CatService.java b/src/java/com/android/internal/telephony/cat/CatService.java
index 1ec2549..f149c6f 100644
--- a/src/java/com/android/internal/telephony/cat/CatService.java
+++ b/src/java/com/android/internal/telephony/cat/CatService.java
@@ -16,12 +16,9 @@
 
 package com.android.internal.telephony.cat;
 
-import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants
-        .IDLE_SCREEN_AVAILABLE_EVENT;
-import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants
-        .LANGUAGE_SELECTION_EVENT;
-import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants
-        .USER_ACTIVITY_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants.IDLE_SCREEN_AVAILABLE_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants.LANGUAGE_SELECTION_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants.USER_ACTIVITY_EVENT;
 
 import android.annotation.UnsupportedAppUsage;
 import android.app.ActivityManagerNative;
@@ -38,7 +35,6 @@
 import android.os.LocaleList;
 import android.os.Message;
 import android.os.RemoteException;
-import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
 import com.android.internal.telephony.CommandsInterface;
@@ -252,6 +248,7 @@
     }
 
     @UnsupportedAppUsage
+    @Override
     public void dispose() {
         synchronized (sInstanceLock) {
             CatLog.d(this, "Disposing CatService object");
@@ -275,7 +272,7 @@
             mMsgDecoder = null;
             removeCallbacksAndMessages(null);
             if (sInstance != null) {
-                if (SubscriptionManager.isValidSlotIndex(mSlotId)) {
+                if (mSlotId >= 0 && mSlotId < sInstance.length) {
                     sInstance[mSlotId] = null;
                 } else {
                     CatLog.d(this, "error: invaild slot id: " + mSlotId);
diff --git a/src/java/com/android/internal/telephony/cat/ResponseData.java b/src/java/com/android/internal/telephony/cat/ResponseData.java
index 600514f..4d0a2d2 100644
--- a/src/java/com/android/internal/telephony/cat/ResponseData.java
+++ b/src/java/com/android/internal/telephony/cat/ResponseData.java
@@ -16,20 +16,25 @@
 
 package com.android.internal.telephony.cat;
 
-import com.android.internal.telephony.EncodeException;
-import com.android.internal.telephony.GsmAlphabet;
-import java.util.Calendar;
-import java.util.TimeZone;
+import android.annotation.UnsupportedAppUsage;
 import android.os.SystemProperties;
 import android.text.TextUtils;
 
+import com.android.internal.telephony.EncodeException;
+import com.android.internal.telephony.GsmAlphabet;
 import com.android.internal.telephony.cat.AppInterface.CommandType;
 
-import android.annotation.UnsupportedAppUsage;
 import java.io.ByteArrayOutputStream;
 import java.io.UnsupportedEncodingException;
+import java.util.Calendar;
+import java.util.TimeZone;
 
 abstract class ResponseData {
+
+    @UnsupportedAppUsage
+    ResponseData() {
+    }
+
     /**
      * Format the data appropriate for TERMINAL RESPONSE and write it into
      * the ByteArrayOutputStream object.
diff --git a/src/java/com/android/internal/telephony/cat/ResultCode.java b/src/java/com/android/internal/telephony/cat/ResultCode.java
index 346d74a..adcf53e 100644
--- a/src/java/com/android/internal/telephony/cat/ResultCode.java
+++ b/src/java/com/android/internal/telephony/cat/ResultCode.java
@@ -26,6 +26,7 @@
  *
  * {@hide}
  */
+@UnsupportedAppUsage(implicitMember = "values()[Lcom/android/internal/telephony/cat/ResultCode;")
 public enum ResultCode {
 
     /*
diff --git a/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java b/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java
index 508908e..eeb9dd4 100755
--- a/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java
+++ b/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java
@@ -22,7 +22,6 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 
-import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.uicc.IccFileHandler;
 import com.android.internal.telephony.uicc.IccUtils;
 import com.android.internal.util.State;
@@ -41,6 +40,7 @@
     // members
     @UnsupportedAppUsage
     private CommandParamsFactory mCmdParamsFactory = null;
+    @UnsupportedAppUsage
     private RilMessage mCurrentRilMessage = null;
     private Handler mCaller = null;
     private static int mSimCount = 0;
@@ -88,6 +88,7 @@
      *
      * @param rilMsg
      */
+    @UnsupportedAppUsage
     public void sendStartDecodingMessageParams(RilMessage rilMsg) {
         Message msg = obtainMessage(CMD_START);
         msg.obj = rilMsg;
@@ -107,6 +108,7 @@
         sendMessage(msg);
     }
 
+    @UnsupportedAppUsage
     private void sendCmdForExecution(RilMessage rilMsg) {
         Message msg = mCaller.obtainMessage(CatService.MSG_ID_RIL_MSG_DECODED,
                 new RilMessage(rilMsg));
diff --git a/src/java/com/android/internal/telephony/cat/ValueParser.java b/src/java/com/android/internal/telephony/cat/ValueParser.java
index 4e528b6..03d7f67 100644
--- a/src/java/com/android/internal/telephony/cat/ValueParser.java
+++ b/src/java/com/android/internal/telephony/cat/ValueParser.java
@@ -16,13 +16,14 @@
 
 package com.android.internal.telephony.cat;
 
+import android.annotation.UnsupportedAppUsage;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+
 import com.android.internal.telephony.GsmAlphabet;
 import com.android.internal.telephony.cat.Duration.TimeUnit;
 import com.android.internal.telephony.uicc.IccUtils;
 
-import android.annotation.UnsupportedAppUsage;
-import android.content.res.Resources;
-import android.content.res.Resources.NotFoundException;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.List;
@@ -62,6 +63,7 @@
      *         Command Details object is found, ResultException is thrown.
      * @throws ResultException
      */
+    @UnsupportedAppUsage
     static DeviceIdentities retrieveDeviceIdentities(ComprehensionTlv ctlv)
             throws ResultException {
 
diff --git a/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java b/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
index 4f8530a..0608f88 100644
--- a/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/cdma/CdmaInboundSmsHandler.java
@@ -22,9 +22,13 @@
 import android.content.IntentFilter;
 import android.content.res.Resources;
 import android.os.Message;
+import android.os.RemoteCallback;
+import android.os.SystemProperties;
 import android.provider.Telephony.Sms.Intents;
+import android.telephony.PhoneNumberUtils;
 import android.telephony.SmsCbMessage;
 import android.telephony.TelephonyManager;
+import android.telephony.cdma.CdmaSmsCbProgramResults;
 
 import com.android.internal.telephony.CellBroadcastHandler;
 import com.android.internal.telephony.CommandsInterface;
@@ -36,10 +40,15 @@
 import com.android.internal.telephony.SmsStorageMonitor;
 import com.android.internal.telephony.TelephonyComponentFactory;
 import com.android.internal.telephony.WspTypeDecoder;
+import com.android.internal.telephony.cdma.sms.BearerData;
 import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
 import com.android.internal.telephony.cdma.sms.SmsEnvelope;
 import com.android.internal.util.HexDump;
 
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 
 /**
@@ -50,22 +59,26 @@
     private final CdmaSMSDispatcher mSmsDispatcher;
     private final CdmaServiceCategoryProgramHandler mServiceCategoryProgramHandler;
     private static CdmaCbTestBroadcastReceiver sTestBroadcastReceiver;
+    private static CdmaScpTestBroadcastReceiver sTestScpBroadcastReceiver;
 
     private byte[] mLastDispatchedSmsFingerprint;
     private byte[] mLastAcknowledgedSmsFingerprint;
 
+    // Callback used to process the result of an SCP message
+    private RemoteCallback mScpCallback;
+
     private final boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean(
             com.android.internal.R.bool.config_duplicate_port_omadm_wappush);
 
     // When TEST_MODE is on we allow the test intent to trigger an SMS CB alert
-    private static boolean sEnableCbModule = false;
-    private static final boolean TEST_MODE = true; //STOPSHIP if true
+    private static final boolean TEST_MODE = SystemProperties.getInt("ro.debuggable", 0) == 1;
     private static final String TEST_ACTION = "com.android.internal.telephony.cdma"
             + ".TEST_TRIGGER_CELL_BROADCAST";
+    private static final String SCP_TEST_ACTION = "com.android.internal.telephony.cdma"
+            + ".TEST_TRIGGER_SCP_MESSAGE";
     private static final String TOGGLE_CB_MODULE = "com.android.internal.telephony.cdma"
             + ".TOGGLE_CB_MODULE";
 
-
     /**
      * Create a new inbound SMS handler for CDMA.
      */
@@ -75,10 +88,63 @@
                 CellBroadcastHandler.makeCellBroadcastHandler(context, phone));
         mSmsDispatcher = smsDispatcher;
         mServiceCategoryProgramHandler = CdmaServiceCategoryProgramHandler.makeScpHandler(context,
-                phone.mCi);
+                phone.mCi, phone);
         phone.mCi.setOnNewCdmaSms(getHandler(), EVENT_NEW_SMS, null);
 
         mCellBroadcastServiceManager.enable();
+        mScpCallback = new RemoteCallback(result -> {
+            if (result == null) {
+                loge("SCP results error: missing extras");
+                return;
+            }
+            String sender = result.getString("sender");
+            if (sender == null) {
+                loge("SCP results error: missing sender extra.");
+                return;
+            }
+            ArrayList<CdmaSmsCbProgramResults> results = result.getParcelableArrayList("results");
+            if (results == null) {
+                loge("SCP results error: missing results extra.");
+                return;
+            }
+
+            BearerData bData = new BearerData();
+            bData.messageType = BearerData.MESSAGE_TYPE_SUBMIT;
+            bData.messageId = SmsMessage.getNextMessageId();
+            bData.serviceCategoryProgramResults = results;
+            byte[] encodedBearerData = BearerData.encode(bData);
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
+            DataOutputStream dos = new DataOutputStream(baos);
+            try {
+                dos.writeInt(SmsEnvelope.TELESERVICE_SCPT);
+                dos.writeInt(0); //servicePresent
+                dos.writeInt(0); //serviceCategory
+                CdmaSmsAddress destAddr = CdmaSmsAddress.parse(
+                        PhoneNumberUtils.cdmaCheckAndProcessPlusCodeForSms(sender));
+                dos.write(destAddr.digitMode);
+                dos.write(destAddr.numberMode);
+                dos.write(destAddr.ton); // number_type
+                dos.write(destAddr.numberPlan);
+                dos.write(destAddr.numberOfDigits);
+                dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits
+                // Subaddress is not supported.
+                dos.write(0); //subaddressType
+                dos.write(0); //subaddr_odd
+                dos.write(0); //subaddr_nbr_of_digits
+                dos.write(encodedBearerData.length);
+                dos.write(encodedBearerData, 0, encodedBearerData.length);
+                // Ignore the RIL response. TODO: implement retry if SMS send fails.
+                mPhone.mCi.sendCdmaSms(baos.toByteArray(), null);
+            } catch (IOException e) {
+                loge("exception creating SCP results PDU", e);
+            } finally {
+                try {
+                    dos.close();
+                } catch (IOException ignored) {
+                }
+            }
+        });
         if (TEST_MODE) {
             if (sTestBroadcastReceiver == null) {
                 sTestBroadcastReceiver = new CdmaCbTestBroadcastReceiver();
@@ -87,6 +153,12 @@
                 filter.addAction(TOGGLE_CB_MODULE);
                 context.registerReceiver(sTestBroadcastReceiver, filter);
             }
+            if (sTestScpBroadcastReceiver == null) {
+                sTestScpBroadcastReceiver = new CdmaScpTestBroadcastReceiver();
+                IntentFilter filter = new IntentFilter();
+                filter.addAction(SCP_TEST_ACTION);
+                context.registerReceiver(sTestScpBroadcastReceiver, filter);
+            }
         }
     }
 
@@ -115,6 +187,7 @@
 
     /**
      * Return true if this handler is for 3GPP2 messages; false for 3GPP format.
+     *
      * @return true (3GPP2)
      */
     @Override
@@ -124,6 +197,7 @@
 
     /**
      * Process Cell Broadcast, Voicemail Notification, and other 3GPP/3GPP2-specific messages.
+     *
      * @param smsb the SmsMessageBase object from the RIL
      * @return true if the message was handled here; false to continue processing
      */
@@ -170,7 +244,11 @@
                 break;
 
             case SmsEnvelope.TELESERVICE_SCPT:
-                mServiceCategoryProgramHandler.dispatchSmsMessage(sms);
+                if (sEnableCbModule) {
+                    mCellBroadcastServiceManager.sendCdmaScpMessageToHandler(sms, mScpCallback);
+                } else {
+                    mServiceCategoryProgramHandler.dispatchSmsMessage(sms);
+                }
                 return Intents.RESULT_SMS_HANDLED;
 
             case SmsEnvelope.TELESERVICE_FDEA_WAP:
@@ -220,8 +298,9 @@
 
     /**
      * Send an acknowledge message.
-     * @param success indicates that last message was successfully received.
-     * @param result result code indicating any error
+     *
+     * @param success  indicates that last message was successfully received.
+     * @param result   result code indicating any error
      * @param response callback message sent when operation completes.
      */
     @Override
@@ -237,27 +316,29 @@
 
     /**
      * Convert Android result code to CDMA SMS failure cause.
+     *
      * @param rc the Android SMS intent result value
      * @return 0 for success, or a CDMA SMS failure cause value
      */
     private static int resultToCause(int rc) {
         switch (rc) {
-        case Activity.RESULT_OK:
-        case Intents.RESULT_SMS_HANDLED:
-            // Cause code is ignored on success.
-            return 0;
-        case Intents.RESULT_SMS_OUT_OF_MEMORY:
-            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE;
-        case Intents.RESULT_SMS_UNSUPPORTED:
-            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID;
-        case Intents.RESULT_SMS_GENERIC_ERROR:
-        default:
-            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM;
+            case Activity.RESULT_OK:
+            case Intents.RESULT_SMS_HANDLED:
+                // Cause code is ignored on success.
+                return 0;
+            case Intents.RESULT_SMS_OUT_OF_MEMORY:
+                return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE;
+            case Intents.RESULT_SMS_UNSUPPORTED:
+                return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID;
+            case Intents.RESULT_SMS_GENERIC_ERROR:
+            default:
+                return CommandsInterface.CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM;
         }
     }
 
     /**
      * Handle {@link SmsEnvelope#TELESERVICE_VMN} and {@link SmsEnvelope#TELESERVICE_MWI}.
+     *
      * @param sms the message to process
      */
     private void handleVoicemailTeleservice(SmsMessage sms) {
@@ -285,8 +366,8 @@
      *
      * @param pdu The WAP-WDP PDU segment
      * @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
-     *         {@link Activity#RESULT_OK} if the message has been broadcast
-     *         to applications
+     * {@link Activity#RESULT_OK} if the message has been broadcast
+     * to applications
      */
     private int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address, String dispAddr,
             long timestamp) {
@@ -333,8 +414,10 @@
         System.arraycopy(pdu, index, userData, 0, pdu.length - index);
         InboundSmsTracker tracker = TelephonyComponentFactory.getInstance()
                 .inject(InboundSmsTracker.class.getName()).makeInboundSmsTracker(
-                userData, timestamp, destinationPort, true, address, dispAddr, referenceNumber,
-                segment, totalSegments, true, HexDump.toHexString(userData), false /* isClass0 */,
+                        userData, timestamp, destinationPort, true, address, dispAddr,
+                        referenceNumber,
+                        segment, totalSegments, true, HexDump.toHexString(userData),
+                        false /* isClass0 */,
                         mPhone.getSubId());
 
         // de-duping is done only for text messages
@@ -346,11 +429,12 @@
      * extra port fields.
      * - Some carriers make this mistake.
      * ex: MSGTYPE-TotalSegments-CurrentSegment
-     *       -SourcePortDestPort-SourcePortDestPort-OMADM PDU
+     * -SourcePortDestPort-SourcePortDestPort-OMADM PDU
+     *
      * @param origPdu The WAP-WDP PDU segment
-     * @param index Current Index while parsing the PDU.
+     * @param index   Current Index while parsing the PDU.
      * @return True if OrigPdu is OmaDM Push Message which has duplicate ports.
-     *         False if OrigPdu is NOT OmaDM Push Message which has duplicate ports.
+     * False if OrigPdu is NOT OmaDM Push Message which has duplicate ports.
      */
     private static boolean checkDuplicatePortOmadmWapPush(byte[] origPdu, int index) {
         index += 4;
@@ -418,7 +502,7 @@
             // the CdmaSmsAddress is not used for a test cell broadcast message, but needs to be
             // supplied to avoid a null pointer exception in the platform
             CdmaSmsAddress nonNullAddress = new CdmaSmsAddress();
-            nonNullAddress.origBytes = new byte[] { (byte) 0xFF };
+            nonNullAddress.origBytes = new byte[]{(byte) 0xFF};
             envelope.origAddress = nonNullAddress;
 
             // parse service category from intent
@@ -447,11 +531,74 @@
         @Override
         protected void handleToggleEnable() {
             // sEnableCbModule is already toggled in super class
+            mCellBroadcastServiceManager.enable();
         }
 
         @Override
         protected void handleToggleDisable(Context context) {
             // sEnableCbModule is already toggled in super class
+            mCellBroadcastServiceManager.disable();
+        }
+    }
+
+    /**
+     * A broadcast receiver used for testing CDMA SCP messages. To trigger test CDMA SCP messages
+     * with adb run e.g:
+     *
+     * adb shell am broadcast -a com.android.internal.telephony.cdma.TEST_TRIGGER_SCP_MESSAGE \
+     * --es originating_address_string 1234567890 \
+     * --es bearer_data_string 00031007B0122610880080B2091C5F1D3965DB95054D1CB2E1E883A6F41334E \
+     * 6CA830EEC882872DFC32F2E9E40
+     *
+     * To toggle use the CDMA CB test broadcast receiver.
+     */
+    private class CdmaScpTestBroadcastReceiver extends CbTestBroadcastReceiver {
+
+        CdmaScpTestBroadcastReceiver() {
+            super(SCP_TEST_ACTION, null);
+        }
+
+        @Override
+        protected void handleTestAction(Intent intent) {
+            SmsEnvelope envelope = new SmsEnvelope();
+            // the CdmaSmsAddress is not used for a test SCP message, but needs to be supplied to
+            // avoid a null pointer exception in the platform
+            CdmaSmsAddress nonNullAddress = new CdmaSmsAddress();
+            nonNullAddress.origBytes = new byte[]{(byte) 0xFF};
+            envelope.origAddress = nonNullAddress;
+
+            // parse bearer data from intent
+            String bearerDataString = intent.getStringExtra("bearer_data_string");
+            envelope.bearerData = decodeHexString(bearerDataString);
+            if (envelope.bearerData == null) {
+                log("No bearer data, ignoring SCP test intent");
+                return;
+            }
+
+            CdmaSmsAddress origAddr = new CdmaSmsAddress();
+            String addressString = intent.getStringExtra("originating_address_string");
+            origAddr.origBytes = decodeHexString(addressString);
+            if (origAddr.origBytes == null) {
+                log("No address data, ignoring SCP test intent");
+                return;
+            }
+            SmsMessage sms = new SmsMessage(origAddr, envelope);
+            sms.parseSms();
+            if (sEnableCbModule) {
+                mCellBroadcastServiceManager.sendCdmaScpMessageToHandler(sms, mScpCallback);
+            } else {
+                mServiceCategoryProgramHandler.dispatchSmsMessage(sms);
+            }
+        }
+
+        @Override
+        protected void handleToggleEnable() {
+            // noop
+        }
+
+        @Override
+        protected void handleToggleDisable(Context context) {
+            // noop
         }
     }
 }
diff --git a/src/java/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java b/src/java/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java
index 5412911..3dc728c 100644
--- a/src/java/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java
+++ b/src/java/com/android/internal/telephony/cdma/CdmaServiceCategoryProgramHandler.java
@@ -31,6 +31,7 @@
 import android.telephony.cdma.CdmaSmsCbProgramResults;
 
 import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
 import com.android.internal.telephony.WakeLockStateMachine;
 import com.android.internal.telephony.cdma.sms.BearerData;
 import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
@@ -51,8 +52,9 @@
     /**
      * Create a new CDMA inbound SMS handler.
      */
-    CdmaServiceCategoryProgramHandler(Context context, CommandsInterface commandsInterface) {
-        super("CdmaServiceCategoryProgramHandler", context, null);
+    CdmaServiceCategoryProgramHandler(Context context, CommandsInterface commandsInterface,
+            Phone phone) {
+        super("CdmaServiceCategoryProgramHandler", context, phone);
         mContext = context;
         mCi = commandsInterface;
     }
@@ -64,9 +66,9 @@
      * @return the new SCPD handler
      */
     static CdmaServiceCategoryProgramHandler makeScpHandler(Context context,
-            CommandsInterface commandsInterface) {
+            CommandsInterface commandsInterface, Phone phone) {
         CdmaServiceCategoryProgramHandler handler = new CdmaServiceCategoryProgramHandler(
-                context, commandsInterface);
+                context, commandsInterface, phone);
         handler.start();
         return handler;
     }
diff --git a/src/java/com/android/internal/telephony/dataconnection/DcTracker.java b/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
index 90d0097..d2354e1 100644
--- a/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/src/java/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -964,7 +964,9 @@
 
         private void enableMobileProvisioning() {
             final Message msg = obtainMessage(DctConstants.CMD_ENABLE_MOBILE_PROVISIONING);
-            msg.setData(Bundle.forPair(DctConstants.PROVISIONING_URL_KEY, mProvisionUrl));
+            Bundle bundle = new Bundle(1);
+            bundle.putString(DctConstants.PROVISIONING_URL_KEY, mProvisionUrl);
+            msg.setData(bundle);
             sendMessage(msg);
         }
 
@@ -2176,13 +2178,6 @@
          * desired power state has changed in the interim, we don't want to
          * override it with an unconditional power on.
          */
-
-        int reset = Integer.parseInt(SystemProperties.get("net.ppp.reset-by-timeout", "0"));
-        try {
-            SystemProperties.set("net.ppp.reset-by-timeout", String.valueOf(reset + 1));
-        } catch (RuntimeException ex) {
-            log("Failed to set net.ppp.reset-by-timeout");
-        }
     }
 
     /**
@@ -4136,6 +4131,9 @@
         mEmergencyApn = new ApnSetting.Builder()
                 .setEntryName("Emergency")
                 .setProtocol(ApnSetting.PROTOCOL_IPV4V6)
+                .setRoamingProtocol(ApnSetting.PROTOCOL_IPV4V6)
+                .setNetworkTypeBitmask((int)(TelephonyManager.NETWORK_TYPE_BITMASK_LTE
+                | TelephonyManager.NETWORK_TYPE_BITMASK_IWLAN))
                 .setApnName("sos")
                 .setApnTypeBitmask(ApnSetting.TYPE_EMERGENCY)
                 .build();
@@ -4797,8 +4795,8 @@
                 .setApn(apn.getApnName())
                 .setProtocolType(apn.getProtocol())
                 .setAuthType(apn.getAuthType())
-                .setUserName(apn.getUser())
-                .setPassword(apn.getPassword())
+                .setUserName(apn.getUser() == null ? "" : apn.getUser())
+                .setPassword(apn.getPassword() == null ? "" : apn.getPassword())
                 .setType(profileType)
                 .setMaxConnectionsTime(apn.getMaxConnsTime())
                 .setMaxConnections(apn.getMaxConns())
diff --git a/src/java/com/android/internal/telephony/euicc/EuiccConnector.java b/src/java/com/android/internal/telephony/euicc/EuiccConnector.java
index 647ef57..874bd51 100644
--- a/src/java/com/android/internal/telephony/euicc/EuiccConnector.java
+++ b/src/java/com/android/internal/telephony/euicc/EuiccConnector.java
@@ -69,6 +69,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
+import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.util.IState;
 import com.android.internal.util.State;
 import com.android.internal.util.StateMachine;
@@ -634,9 +635,9 @@
                     isSameComponent = mSelectedComponent != null;
                 } else {
                     isSameComponent = mSelectedComponent == null
-                            || Objects.equals(
-                                    bestComponent.getComponentName(),
-                                    mSelectedComponent.getComponentName());
+                            || Objects.equals(new ComponentName(bestComponent.packageName,
+                            bestComponent.name),
+                        new ComponentName(mSelectedComponent.packageName, mSelectedComponent.name));
                 }
                 boolean forceRebind = bestComponent != null
                         && Objects.equals(bestComponent.packageName, affectedPackage);
@@ -1041,7 +1042,8 @@
             return false;
         }
         Intent intent = new Intent(EuiccService.EUICC_SERVICE_INTERFACE);
-        intent.setComponent(mSelectedComponent.getComponentName());
+        intent.setComponent(new ComponentName(mSelectedComponent.packageName,
+            mSelectedComponent.name));
         // We bind this as a foreground service because it is operating directly on the SIM, and we
         // do not want it subjected to power-savings restrictions while doing so.
         return mContext.bindService(intent, this,
@@ -1065,7 +1067,7 @@
 
                 if (resolveInfo.filter.getPriority() > bestPriority) {
                     bestPriority = resolveInfo.filter.getPriority();
-                    bestComponent = resolveInfo.getComponentInfo();
+                    bestComponent = TelephonyUtils.getComponentInfo(resolveInfo);
                 }
             }
         }
@@ -1075,8 +1077,9 @@
 
     private static boolean isValidEuiccComponent(
             PackageManager packageManager, ResolveInfo resolveInfo) {
-        ComponentInfo componentInfo = resolveInfo.getComponentInfo();
-        String packageName = componentInfo.getComponentName().getPackageName();
+        ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(resolveInfo);
+        String packageName = new ComponentName(componentInfo.packageName, componentInfo.name)
+            .getPackageName();
 
         // Verify that the app is privileged (via granting of a privileged permission).
         if (packageManager.checkPermission(
diff --git a/src/java/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java b/src/java/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
index b9d4171..052d89c 100644
--- a/src/java/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
+++ b/src/java/com/android/internal/telephony/gsm/GsmCellBroadcastHandler.java
@@ -39,6 +39,8 @@
 import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage;
 import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -54,6 +56,7 @@
     private static final String MESSAGE_NOT_BROADCASTED = "0";
 
     /** This map holds incomplete concatenated messages waiting for assembly. */
+    @UnsupportedAppUsage
     private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
             new HashMap<SmsCbConcatInfo, byte[][]>(4);
 
@@ -334,6 +337,7 @@
         private final SmsCbHeader mHeader;
         private final SmsCbLocation mLocation;
 
+        @UnsupportedAppUsage
         SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
             mHeader = header;
             mLocation = location;
@@ -369,6 +373,7 @@
          * @param cid the current Cell ID
          * @return true if this message is valid for the current location; false otherwise
          */
+        @UnsupportedAppUsage
         public boolean matchesLocation(String plmn, int lac, int cid) {
             return mLocation.isInLocationArea(plmn, lac, cid);
         }
diff --git a/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java b/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
index b5413f6..7b5dd0c 100644
--- a/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
+++ b/src/java/com/android/internal/telephony/gsm/GsmInboundSmsHandler.java
@@ -23,6 +23,7 @@
 import android.content.IntentFilter;
 import android.os.AsyncResult;
 import android.os.Message;
+import android.os.SystemProperties;
 import android.provider.Telephony.Sms.Intents;
 
 import com.android.internal.telephony.CommandsInterface;
@@ -35,6 +36,8 @@
 import com.android.internal.telephony.VisualVoicemailSmsFilter;
 import com.android.internal.telephony.uicc.UsimServiceTable;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 /**
  * This class broadcasts incoming SMS messages to interested apps after storing them in
  * the SmsProvider "raw" table and ACKing them to the SMSC. After each message has been
@@ -46,7 +49,7 @@
     private final UsimDataDownloadHandler mDataDownloadHandler;
 
     // When TEST_MODE is on we allow the test intent to trigger an SMS CB alert
-    private static final boolean TEST_MODE = true; //STOPSHIP if true
+    private static final boolean TEST_MODE = SystemProperties.getInt("ro.debuggable", 0) == 1;
     private static final String TEST_ACTION = "com.android.internal.telephony.gsm"
             + ".TEST_TRIGGER_CELL_BROADCAST";
     private static final String TOGGLE_CB_MODULE = "com.android.internal.telephony.gsm"
@@ -264,6 +267,7 @@
      * @param result result code indicating any error
      * @param response callback message sent when operation completes.
      */
+    @UnsupportedAppUsage
     @Override
     protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) {
         mPhone.mCi.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response);
diff --git a/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java b/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java
index 6e96e25a..01ba6ad 100644
--- a/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java
+++ b/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java
@@ -55,6 +55,8 @@
 import com.android.internal.telephony.uicc.UiccCardApplication;
 import com.android.internal.util.ArrayUtils;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -136,15 +138,25 @@
 
     //***** Instance Variables
 
+    @UnsupportedAppUsage
     GsmCdmaPhone mPhone;
+    @UnsupportedAppUsage
     Context mContext;
     UiccCardApplication mUiccApplication;
+    @UnsupportedAppUsage
     IccRecords mIccRecords;
 
     String mAction;              // One of ACTION_*
+    @UnsupportedAppUsage
     String mSc;                  // Service Code
-    String mSia, mSib, mSic;       // Service Info a,b,c
+    @UnsupportedAppUsage
+    String mSia;                 // Service Info a
+    @UnsupportedAppUsage
+    String  mSib;                // Service Info b
+    @UnsupportedAppUsage
+    String mSic;                 // Service Info c
     String mPoundString;         // Entire MMI string up to and including #
+    @UnsupportedAppUsage
     public String mDialingNumber;
     String mPwd;                 // For password registration
 
@@ -165,6 +177,7 @@
 
     // See TS 22.030 6.5.2 "Structure of the MMI"
 
+    @UnsupportedAppUsage
     static Pattern sPatternSuppService = Pattern.compile(
         "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)");
 /*       1  2                    3          4  5       6   7         8    9     10  11             12
@@ -205,6 +218,7 @@
      *
      * Please see flow chart in TS 22.030 6.5.3.2
      */
+    @UnsupportedAppUsage
     public static GsmMmiCode newFromDialString(String dialString, GsmCdmaPhone phone,
             UiccCardApplication app) {
         return newFromDialString(dialString, phone, app, null);
@@ -461,6 +475,7 @@
     /** make empty strings be null.
      *  Regexp returns empty strings for empty groups
      */
+    @UnsupportedAppUsage
     private static String
     makeEmptyNull (String s) {
         if (s != null && s.length() == 0) return null;
@@ -498,6 +513,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     private static int
     siToServiceClass(String si) {
         if (si == null || si.length() == 0) {
@@ -547,6 +563,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     static boolean
     isServiceCodeCallForwarding(String sc) {
         return sc != null &&
@@ -556,6 +573,7 @@
                 || sc.equals(SC_CF_All_Conditional));
     }
 
+    @UnsupportedAppUsage
     static boolean
     isServiceCodeCallBarring(String sc) {
         Resources resource = Resources.getSystem();
@@ -600,6 +618,7 @@
 
     //***** Constructor
 
+    @UnsupportedAppUsage
     public GsmMmiCode(GsmCdmaPhone phone, UiccCardApplication app) {
         // The telephony unit-test cases may create GsmMmiCode's
         // in secondary threads
@@ -782,6 +801,7 @@
      *  In temporary mode, to invoke CLIR for a single call enter:
      *       " # 31 # [called number] SEND "
      */
+    @UnsupportedAppUsage
     public boolean
     isTemporaryModeCLIR() {
         return mSc != null && mSc.equals(SC_CLIR) && mDialingNumber != null
@@ -792,6 +812,7 @@
      * returns CommandsInterface.CLIR_*
      * See also isTemporaryModeCLIR()
      */
+    @UnsupportedAppUsage
     public int
     getCLIRMode() {
         if (mSc != null && mSc.equals(SC_CLIR)) {
@@ -829,22 +850,27 @@
         return false;
     }
 
+    @UnsupportedAppUsage
     boolean isActivate() {
         return mAction != null && mAction.equals(ACTION_ACTIVATE);
     }
 
+    @UnsupportedAppUsage
     boolean isDeactivate() {
         return mAction != null && mAction.equals(ACTION_DEACTIVATE);
     }
 
+    @UnsupportedAppUsage
     boolean isInterrogate() {
         return mAction != null && mAction.equals(ACTION_INTERROGATE);
     }
 
+    @UnsupportedAppUsage
     boolean isRegister() {
         return mAction != null && mAction.equals(ACTION_REGISTER);
     }
 
+    @UnsupportedAppUsage
     boolean isErasure() {
         return mAction != null && mAction.equals(ACTION_ERASURE);
     }
@@ -874,6 +900,7 @@
     }
 
     /** Process a MMI code or short code...anything that isn't a dialing number */
+    @UnsupportedAppUsage
     public void
     processCode() throws CallStateException {
         try {
@@ -1264,6 +1291,7 @@
         return mContext.getText(com.android.internal.R.string.mmiError);
     }
 
+    @UnsupportedAppUsage
     private CharSequence getScString() {
         if (mSc != null) {
             if (isServiceCodeCallBarring(mSc)) {
diff --git a/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
index e9c5403..0e5d83f 100644
--- a/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
+++ b/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
@@ -28,9 +28,9 @@
 import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
 import com.android.internal.telephony.InboundSmsHandler;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.SMSDispatcher;
 import com.android.internal.telephony.SmsConstants;
 import com.android.internal.telephony.SmsDispatchersController;
-import com.android.internal.telephony.SMSDispatcher;
 import com.android.internal.telephony.SmsHeader;
 import com.android.internal.telephony.SmsMessageBase;
 import com.android.internal.telephony.uicc.IccRecords;
@@ -39,6 +39,8 @@
 import com.android.internal.telephony.uicc.UiccController;
 import com.android.internal.telephony.util.SMSDispatcherUtil;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.util.HashMap;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -48,6 +50,7 @@
     private AtomicReference<IccRecords> mIccRecords = new AtomicReference<IccRecords>();
     private AtomicReference<UiccCardApplication> mUiccApplication =
             new AtomicReference<UiccCardApplication>();
+    @UnsupportedAppUsage
     private GsmInboundSmsHandler mGsmInboundSmsHandler;
 
     /** Status report received */
@@ -70,6 +73,7 @@
         mUiccController.unregisterForIccChanged(this);
     }
 
+    @UnsupportedAppUsage
     @Override
     protected String getFormat() {
         return SmsConstants.FORMAT_3GPP;
@@ -166,6 +170,7 @@
     }
 
     /** {@inheritDoc} */
+    @UnsupportedAppUsage
     @Override
     protected void sendSms(SmsTracker tracker) {
         int ss = mPhone.getServiceState().getState();
diff --git a/src/java/com/android/internal/telephony/gsm/SimTlv.java b/src/java/com/android/internal/telephony/gsm/SimTlv.java
index c98b9a1..7df1f96 100644
--- a/src/java/com/android/internal/telephony/gsm/SimTlv.java
+++ b/src/java/com/android/internal/telephony/gsm/SimTlv.java
@@ -16,6 +16,8 @@
 
 package com.android.internal.telephony.gsm;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 /**
  * SIM Tag-Length-Value record
  * TS 102 223 Annex C
@@ -33,8 +35,10 @@
     int mCurOffset;
     int mCurDataOffset;
     int mCurDataLength;
+    @UnsupportedAppUsage
     boolean mHasValidTlvObject;
 
+    @UnsupportedAppUsage
     public SimTlv(byte[] record, int offset, int length) {
         mRecord = record;
 
@@ -45,6 +49,7 @@
         mHasValidTlvObject = parseCurrentTlvObject();
     }
 
+    @UnsupportedAppUsage
     public boolean nextObject() {
         if (!mHasValidTlvObject) return false;
         mCurOffset = mCurDataOffset + mCurDataLength;
@@ -52,6 +57,7 @@
         return mHasValidTlvObject;
     }
 
+    @UnsupportedAppUsage
     public boolean isValidObject() {
         return mHasValidTlvObject;
     }
@@ -62,6 +68,7 @@
      * 0 and 0xff are invalid tag values
      * valid tags range from 1 - 0xfe
      */
+    @UnsupportedAppUsage
     public int getTag() {
         if (!mHasValidTlvObject) return 0;
         return mRecord[mCurOffset] & 0xff;
@@ -72,6 +79,7 @@
      * returns null if !isValidObject()
      */
 
+    @UnsupportedAppUsage
     public byte[] getData() {
         if (!mHasValidTlvObject) return null;
 
diff --git a/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java b/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java
index 6489014..a541459 100755
--- a/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java
+++ b/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java
@@ -28,6 +28,9 @@
 import com.android.internal.telephony.uicc.IccConstants;
 import com.android.internal.telephony.uicc.IccFileHandler;
 import com.android.internal.telephony.uicc.IccUtils;
+
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.util.ArrayList;
 
 /**
@@ -41,9 +44,12 @@
     private static final boolean DBG = true;
     private ArrayList<PbrRecord> mPbrRecords;
     private Boolean mIsPbrPresent;
+    @UnsupportedAppUsage
     private IccFileHandler mFh;
     private AdnRecordCache mAdnCache;
+    @UnsupportedAppUsage
     private Object mLock = new Object();
+    @UnsupportedAppUsage
     private ArrayList<AdnRecord> mPhoneBookRecords;
     private ArrayList<byte[]> mIapFileRecord;
     private ArrayList<byte[]> mEmailFileRecord;
@@ -119,6 +125,7 @@
         mSfiEfidTable = new SparseIntArray();
     }
 
+    @UnsupportedAppUsage
     public void reset() {
         mPhoneBookRecords.clear();
         mIapFileRecord = null;
@@ -131,6 +138,7 @@
     }
 
     // Load all phonebook related EFs from the SIM.
+    @UnsupportedAppUsage
     public ArrayList<AdnRecord> loadEfFilesFromUsim() {
         synchronized (mLock) {
             if (!mPhoneBookRecords.isEmpty()) {
@@ -660,6 +668,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     private void log(String msg) {
         if(DBG) Rlog.d(LOG_TAG, msg);
     }
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
index e0e1a23..7a863d0 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhone.java
@@ -237,6 +237,18 @@
         this.mCurrentSubscriberUris = currentSubscriberUris;
     }
 
+    @UnsupportedAppUsage
+    @Override
+    public void notifyCallForwardingIndicator() {
+        super.notifyCallForwardingIndicator();
+    }
+
+    @UnsupportedAppUsage
+    @Override
+    public void notifyPreciseCallStateChanged() {
+        super.notifyPreciseCallStateChanged();
+    }
+
     @Override
     public Uri[] getCurrentSubscriberUris() {
         return mCurrentSubscriberUris;
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCall.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCall.java
index eb01a34..f2fb779 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCall.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCall.java
@@ -18,18 +18,18 @@
 
 import android.annotation.UnsupportedAppUsage;
 import android.telecom.ConferenceParticipant;
-import android.telephony.Rlog;
 import android.telephony.DisconnectCause;
+import android.telephony.Rlog;
+import android.telephony.ims.ImsStreamMediaProfile;
 import android.util.Log;
 
+import com.android.ims.ImsCall;
+import com.android.ims.ImsException;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
 import com.android.internal.telephony.Connection;
 import com.android.internal.telephony.Phone;
-import com.android.ims.ImsCall;
-import com.android.ims.ImsException;
-import android.telephony.ims.ImsStreamMediaProfile;
 
 import java.util.List;
 
@@ -300,6 +300,7 @@
      * @return The {@link ImsCall}.
      */
     @VisibleForTesting
+    @UnsupportedAppUsage
     public ImsCall
     getImsCall() {
         return (getFirstConnection() == null) ? null : getFirstConnection().getImsCall();
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index 9c5202c..ec3f324 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -399,7 +399,7 @@
     private int mClirMode = CommandsInterface.CLIR_DEFAULT;
     @UnsupportedAppUsage
     private Object mSyncHold = new Object();
-
+    @UnsupportedAppUsage
     private ImsCall mUssdSession = null;
     @UnsupportedAppUsage
     private Message mPendingUssd = null;
@@ -415,6 +415,7 @@
 
     private PhoneConstants.State mState = PhoneConstants.State.IDLE;
 
+    @UnsupportedAppUsage
     private ImsManager mImsManager;
     private ImsUtInterface mUtInterface;
 
@@ -429,6 +430,7 @@
     private boolean pendingCallInEcm = false;
     @UnsupportedAppUsage
     private boolean mSwitchingFgAndBgCalls = false;
+    @UnsupportedAppUsage
     private ImsCall mCallExpectedToResume = null;
     @UnsupportedAppUsage
     private boolean mAllowEmergencyVideoCalls = false;
@@ -1898,6 +1900,7 @@
         mUssdSession.terminate(ImsReasonInfo.CODE_USER_TERMINATED);
     }
 
+    @UnsupportedAppUsage
     private synchronized ImsPhoneConnection findConnection(final ImsCall imsCall) {
         for (ImsPhoneConnection conn : mConnections) {
             if (conn.getImsCall() == imsCall) {
@@ -1937,6 +1940,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause) {
         if (DBG) log("processCallStateChange " + imsCall + " state=" + state + " cause=" + cause);
         // This method is called on onCallUpdate() where there is not necessarily a call state
@@ -1946,6 +1950,7 @@
         processCallStateChange(imsCall, state, cause, false /* do not ignore state update */);
     }
 
+    @UnsupportedAppUsage
     private void processCallStateChange(ImsCall imsCall, ImsPhoneCall.State state, int cause,
             boolean ignoreState) {
         if (DBG) {
@@ -2260,6 +2265,7 @@
     /**
      * Listen to the IMS call state change
      */
+    @UnsupportedAppUsage
     private ImsCall.Listener mImsCallListener = new ImsCall.Listener() {
         @Override
         public void onCallProgressing(ImsCall imsCall) {
@@ -3728,6 +3734,7 @@
     }
 
     /* package */
+    @UnsupportedAppUsage
     ImsEcbm getEcbmInterface() throws ImsException {
         if (mImsManager == null) {
             throw getImsManagerIsNullException();
@@ -3814,6 +3821,7 @@
         mImsManagerConnector.connect();
     }
 
+    @UnsupportedAppUsage
     private void setVideoCallProvider(ImsPhoneConnection conn, ImsCall imsCall)
             throws RemoteException {
         IImsVideoCallProvider imsVideoCallProvider =
diff --git a/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java b/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
index e086f16..f98fc40 100644
--- a/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
+++ b/src/java/com/android/internal/telephony/imsphone/ImsPhoneConnection.java
@@ -66,6 +66,7 @@
     private ImsPhoneCallTracker mOwner;
     @UnsupportedAppUsage
     private ImsPhoneCall mParent;
+    @UnsupportedAppUsage
     private ImsCall mImsCall;
     private Bundle mExtras = new Bundle();
     private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
@@ -727,6 +728,7 @@
      * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been
      *     changed, and {@code false} otherwise.
      */
+    @UnsupportedAppUsage
     public boolean update(ImsCall imsCall, ImsPhoneCall.State state) {
         if (state == ImsPhoneCall.State.ACTIVE) {
             // If the state of the call is active, but there is a pending request to the RIL to hold
diff --git a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
index b8dbcf0..2674290 100644
--- a/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/src/java/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -72,6 +72,7 @@
 import com.android.internal.telephony.RIL;
 import com.android.internal.telephony.RILConstants;
 import com.android.internal.telephony.SmsResponse;
+import com.android.internal.telephony.util.TelephonyUtils;
 import com.android.internal.telephony.UUSInfo;
 import com.android.internal.telephony.imsphone.ImsPhoneCall;
 import com.android.internal.telephony.nano.TelephonyProto;
@@ -2469,14 +2470,14 @@
         }
 
         // fill in complete matching information from the SIM.
-        carrierIdMatchingResult.mccmnc = TextUtils.emptyIfNull(simInfo.mccMnc);
-        carrierIdMatchingResult.spn = TextUtils.emptyIfNull(simInfo.spn);
-        carrierIdMatchingResult.pnn = TextUtils.emptyIfNull(simInfo.plmn);
-        carrierIdMatchingResult.gid1 = TextUtils.emptyIfNull(simInfo.gid1);
-        carrierIdMatchingResult.gid2 = TextUtils.emptyIfNull(simInfo.gid2);
-        carrierIdMatchingResult.imsiPrefix = TextUtils.emptyIfNull(simInfo.imsiPrefixPattern);
-        carrierIdMatchingResult.iccidPrefix = TextUtils.emptyIfNull(simInfo.iccidPrefix);
-        carrierIdMatchingResult.preferApn = TextUtils.emptyIfNull(simInfo.apn);
+        carrierIdMatchingResult.mccmnc = TelephonyUtils.emptyIfNull(simInfo.mccMnc);
+        carrierIdMatchingResult.spn = TelephonyUtils.emptyIfNull(simInfo.spn);
+        carrierIdMatchingResult.pnn = TelephonyUtils.emptyIfNull(simInfo.plmn);
+        carrierIdMatchingResult.gid1 = TelephonyUtils.emptyIfNull(simInfo.gid1);
+        carrierIdMatchingResult.gid2 = TelephonyUtils.emptyIfNull(simInfo.gid2);
+        carrierIdMatchingResult.imsiPrefix = TelephonyUtils.emptyIfNull(simInfo.imsiPrefixPattern);
+        carrierIdMatchingResult.iccidPrefix = TelephonyUtils.emptyIfNull(simInfo.iccidPrefix);
+        carrierIdMatchingResult.preferApn = TelephonyUtils.emptyIfNull(simInfo.apn);
         if (simInfo.privilegeAccessRule != null) {
             carrierIdMatchingResult.privilegeAccessRule =
                     simInfo.privilegeAccessRule.stream().toArray(String[]::new);
diff --git a/src/java/com/android/internal/telephony/nitz/NewNitzStateMachineImpl.java b/src/java/com/android/internal/telephony/nitz/NewNitzStateMachineImpl.java
new file mode 100644
index 0000000..edc3e67
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/NewNitzStateMachineImpl.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.timedetector.PhoneTimeSuggestion;
+import android.content.Context;
+import android.telephony.Rlog;
+import android.util.TimestampedValue;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachine;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TimeZoneLookupHelper;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.Objects;
+
+// TODO Update this comment when NitzStateMachineImpl is deleted - it will no longer be appropriate
+// to contrast the behavior of the two implementations.
+/**
+ * A new and more testable implementation of {@link NitzStateMachine}. It is intended to replace
+ * {@link com.android.internal.telephony.NitzStateMachineImpl}.
+ *
+ * <p>This implementation differs in a number of ways:
+ * <ul>
+ *     <li>It is decomposed into multiple classes that perform specific, well-defined, usually
+ *     stateless, testable behaviors.
+ *     </li>
+ *     <li>It splits responsibility for setting the device time zone with a "time zone detection
+ *     service". The time zone detection service is stateful, recording the latest suggestion from
+ *     possibly multiple sources. The {@link NewNitzStateMachineImpl} must now actively signal when
+ *     it has no answer for the current time zone, allowing the service to arbitrate between
+ *     multiple sources without polling each of them.
+ *     </li>
+ *     <li>Rate limiting of NITZ signals is performed for time zone as well as time detection.</li>
+ * </ul>
+ */
+public final class NewNitzStateMachineImpl implements NitzStateMachine {
+
+    /**
+     * An interface for predicates applied to incoming NITZ signals to determine whether they must
+     * be processed. See {@link NitzSignalInputFilterPredicateFactory#create(Context, DeviceState)}
+     * for the real implementation. The use of an interface means the behavior can be tested
+     * independently and easily replaced for tests.
+     */
+    @VisibleForTesting
+    @FunctionalInterface
+    public interface NitzSignalInputFilterPredicate {
+
+        /**
+         * See {@link NitzSignalInputFilterPredicate}.
+         */
+        boolean mustProcessNitzSignal(
+                @Nullable TimestampedValue<NitzData> oldSignal,
+                @NonNull TimestampedValue<NitzData> newSignal);
+    }
+
+    /**
+     * An interface for the stateless component that generates suggestions using country and/or NITZ
+     * information. The use of an interface means the behavior can be tested independently.
+     */
+    @VisibleForTesting
+    public interface TimeZoneSuggester {
+
+        /**
+         * Generates a {@link PhoneTimeZoneSuggestion} given the information available. This method
+         * must always return a non-null {@link PhoneTimeZoneSuggestion} but that object does not
+         * have to contain a time zone if the available information is not sufficient to determine
+         * one. {@link PhoneTimeZoneSuggestion#getDebugInfo()} provides debugging / logging
+         * information explaining the choice.
+         */
+        @NonNull
+        PhoneTimeZoneSuggestion getTimeZoneSuggestion(
+                int phoneId, @Nullable String countryIsoCode,
+                @Nullable TimestampedValue<NitzData> nitzSignal);
+    }
+
+    static final String LOG_TAG = "NewNitzStateMachineImpl";
+    static final boolean DBG = true;
+
+    // Miscellaneous dependencies and helpers not related to detection state.
+    private final int mPhoneId;
+    /** Accesses global information about the device. */
+    private final DeviceState mDeviceState;
+    /** Applied to NITZ signals during input filtering. */
+    private final NitzSignalInputFilterPredicate mNitzSignalInputFilter;
+    /** Creates {@link PhoneTimeZoneSuggestion} for passing to the time zone detection service. */
+    private final TimeZoneSuggester mTimeZoneSuggester;
+    /** A facade to the time / time zone detection services. */
+    private final NewTimeServiceHelper mNewTimeServiceHelper;
+
+    // Shared detection state.
+
+    /**
+     * The last / latest NITZ signal <em>processed</em> (i.e. after input filtering). It is used for
+     * input filtering (e.g. rate limiting) and provides the NITZ information when time / time zone
+     * needs to be recalculated when something else has changed.
+     */
+    @Nullable
+    private TimestampedValue<NitzData> mLatestNitzSignal;
+
+    // Time Zone detection state.
+
+    /**
+     * Records whether the device should have a country code available via
+     * {@link DeviceState#getNetworkCountryIsoForPhone()}. Before this an NITZ signal
+     * received is (almost always) not enough to determine time zone. On test networks the country
+     * code should be available but can still be an empty string but this flag indicates that the
+     * information available is unlikely to improve.
+     */
+    private boolean mGotCountryCode = false;
+
+    /**
+     * Creates an instance for the supplied {@link Phone}.
+     */
+    public static NewNitzStateMachineImpl createInstance(@NonNull Phone phone) {
+        Objects.requireNonNull(phone);
+
+        int phoneId = phone.getPhoneId();
+        DeviceState deviceState = new DeviceStateImpl(phone);
+        TimeZoneLookupHelper timeZoneLookupHelper = new TimeZoneLookupHelper();
+        TimeZoneSuggester timeZoneSuggester =
+                new TimeZoneSuggesterImpl(deviceState, timeZoneLookupHelper);
+        NewTimeServiceHelper newTimeServiceHelper = new NewTimeServiceHelperImpl(phone);
+        NitzSignalInputFilterPredicate nitzSignalFilter =
+                NitzSignalInputFilterPredicateFactory.create(phone.getContext(), deviceState);
+        return new NewNitzStateMachineImpl(
+                phoneId, nitzSignalFilter, timeZoneSuggester, newTimeServiceHelper, deviceState);
+    }
+
+    /**
+     * Creates an instance using the supplied components. Used during tests to supply fakes.
+     * See {@link #createInstance(Phone)}
+     */
+    @VisibleForTesting
+    public NewNitzStateMachineImpl(int phoneId,
+            @NonNull NitzSignalInputFilterPredicate nitzSignalInputFilter,
+            @NonNull TimeZoneSuggester timeZoneSuggester,
+            @NonNull NewTimeServiceHelper newTimeServiceHelper, @NonNull DeviceState deviceState) {
+        mPhoneId = phoneId;
+        mTimeZoneSuggester = Objects.requireNonNull(timeZoneSuggester);
+        mNewTimeServiceHelper = Objects.requireNonNull(newTimeServiceHelper);
+        mDeviceState = Objects.requireNonNull(deviceState);
+        mNitzSignalInputFilter = Objects.requireNonNull(nitzSignalInputFilter);
+    }
+
+    @Override
+    public void handleNetworkAvailable() {
+        // Assume any previous NITZ signals received are now invalid.
+        mLatestNitzSignal = null;
+
+        String countryIsoCode =
+                mGotCountryCode ? mDeviceState.getNetworkCountryIsoForPhone() : null;
+
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleNetworkAvailable: countryIsoCode=" + countryIsoCode
+                    + ", mLatestNitzSignal=" + mLatestNitzSignal);
+        }
+
+        String reason = "handleNetworkAvailable()";
+
+        // Generate a new time zone suggestion and update the service as needed.
+        doTimeZoneDetection(countryIsoCode, null /* nitzSignal */, reason);
+
+        // Generate a new time suggestion and update the service as needed.
+        doTimeDetection(null /* nitzSignal */, reason);
+    }
+
+    @Override
+    public void handleNetworkCountryCodeSet(boolean countryChanged) {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: countryChanged=" + countryChanged
+                    + ", mLatestNitzSignal=" + mLatestNitzSignal);
+        }
+
+        mGotCountryCode = true;
+
+        // Generate a new time zone suggestion and update the service as needed.
+        String countryIsoCode = mDeviceState.getNetworkCountryIsoForPhone();
+        doTimeZoneDetection(countryIsoCode, mLatestNitzSignal,
+                "handleNetworkCountryCodeSet(" + countryChanged + ")");
+    }
+
+    @Override
+    public void handleNetworkCountryCodeUnavailable() {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleNetworkCountryCodeUnavailable:"
+                    + " mLatestNitzSignal=" + mLatestNitzSignal);
+        }
+        mGotCountryCode = false;
+
+        // Generate a new time zone suggestion and update the service as needed.
+        doTimeZoneDetection(null /* countryIsoCode */, mLatestNitzSignal,
+                "handleNetworkCountryCodeUnavailable()");
+    }
+
+    @Override
+    public void handleNitzReceived(@NonNull TimestampedValue<NitzData> nitzSignal) {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleNitzReceived: nitzSignal=" + nitzSignal);
+        }
+        Objects.requireNonNull(nitzSignal);
+
+        // Perform input filtering to filter bad data and avoid processing signals too often.
+        TimestampedValue<NitzData> previousNitzSignal = mLatestNitzSignal;
+        if (!mNitzSignalInputFilter.mustProcessNitzSignal(previousNitzSignal, nitzSignal)) {
+            return;
+        }
+
+        // Always store the latest valid NITZ signal to be processed.
+        mLatestNitzSignal = nitzSignal;
+
+        String reason = "handleNitzReceived(" + nitzSignal + ")";
+
+        // Generate a new time zone suggestion and update the service as needed.
+        String countryIsoCode =
+                mGotCountryCode ? mDeviceState.getNetworkCountryIsoForPhone() : null;
+        doTimeZoneDetection(countryIsoCode, nitzSignal, reason);
+
+        // Generate a new time suggestion and update the service as needed.
+        doTimeDetection(nitzSignal, reason);
+    }
+
+    @Override
+    public void handleAirplaneModeChanged(boolean on) {
+        if (DBG) {
+            Rlog.d(LOG_TAG, "handleAirplaneModeChanged: on=" + on);
+        }
+
+        // Treat entry / exit from airplane mode as a strong signal that the user wants to clear
+        // cached state. If the user really is boarding a plane they won't want cached state from
+        // before their flight influencing behavior.
+        //
+        // State is cleared on entry AND exit: on entry because the detection code shouldn't be
+        // opinionated while in airplane mode, and on exit to avoid any unexpected signals received
+        // while in airplane mode from influencing behavior afterwards.
+        //
+        // After clearing detection state, the time zone detection should work out from first
+        // principles what the time / time zone is. This assumes calls like handleNetworkAvailable()
+        // will be made after airplane mode is re-enabled as the device re-establishes network
+        // connectivity.
+
+        // Clear shared state.
+        mLatestNitzSignal = null;
+
+        // Clear time zone detection state.
+        mGotCountryCode = false;
+
+        String reason = "handleAirplaneModeChanged(" + on + ")";
+
+        // Generate a new time zone suggestion and update the service as needed.
+        doTimeZoneDetection(null /* countryIsoCode */, null /* nitzSignal */,
+                reason);
+
+        // Generate a new time suggestion and update the service as needed.
+        doTimeDetection(null /* nitzSignal */, reason);
+    }
+
+    /**
+     * Perform a round of time zone detection and notify the time zone detection service as needed.
+     */
+    private void doTimeZoneDetection(
+            @Nullable String countryIsoCode, @Nullable TimestampedValue<NitzData> nitzSignal,
+            @NonNull String reason) {
+        try {
+            Objects.requireNonNull(reason);
+
+            PhoneTimeZoneSuggestion suggestion =
+                    mTimeZoneSuggester.getTimeZoneSuggestion(mPhoneId, countryIsoCode, nitzSignal);
+            suggestion.addDebugInfo("Detection reason=" + reason);
+            if (DBG) {
+                Rlog.d(LOG_TAG, "doTimeZoneDetection: countryIsoCode=" + countryIsoCode
+                        + ", nitzSignal=" + nitzSignal + ", suggestion=" + suggestion
+                        + ", reason=" + reason);
+            }
+            mNewTimeServiceHelper.maybeSuggestDeviceTimeZone(suggestion);
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "doTimeZoneDetection: Exception thrown"
+                    + " mPhoneId=" + mPhoneId
+                    + ", countryIsoCode=" + countryIsoCode
+                    + ", nitzSignal=" + nitzSignal
+                    + ", reason=" + reason
+                    + ", ex=" + ex, ex);
+        }
+    }
+
+    /**
+     * Perform a round of time detection and notify the time detection service as needed.
+     */
+    private void doTimeDetection(@Nullable TimestampedValue<NitzData> nitzSignal,
+            @NonNull String reason) {
+        try {
+            Objects.requireNonNull(reason);
+            if (nitzSignal == null) {
+                // Do nothing to withdraw previous suggestions: the service currently does not
+                // support withdrawing suggestions.
+                return;
+            }
+
+            Objects.requireNonNull(nitzSignal.getValue());
+
+            TimestampedValue<Long> newNitzTime = new TimestampedValue<>(
+                    nitzSignal.getReferenceTimeMillis(),
+                    nitzSignal.getValue().getCurrentTimeInMillis());
+            PhoneTimeSuggestion timeSuggestion = new PhoneTimeSuggestion(mPhoneId, newNitzTime);
+            timeSuggestion.addDebugInfo("doTimeDetection: NITZ signal used"
+                    + " nitzSignal=" + nitzSignal
+                    + ", newNitzTime=" + newNitzTime
+                    + ", reason=" + reason);
+            mNewTimeServiceHelper.suggestDeviceTime(timeSuggestion);
+        } catch (RuntimeException ex) {
+            Rlog.e(LOG_TAG, "doTimeDetection: Exception thrown"
+                    + " mPhoneId=" + mPhoneId
+                    + ", nitzSignal=" + nitzSignal
+                    + ", reason=" + reason
+                    + ", ex=" + ex, ex);
+        }
+    }
+
+    @Override
+    public void dumpState(PrintWriter pw) {
+        pw.println(" NewNitzStateMachineImpl.mLatestNitzSignal=" + mLatestNitzSignal);
+        pw.println(" NewNitzStateMachineImpl.mGotCountryCode=" + mGotCountryCode);
+        mNewTimeServiceHelper.dumpState(pw);
+        pw.flush();
+    }
+
+    @Override
+    public void dumpLogs(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
+        mNewTimeServiceHelper.dumpLogs(ipw);
+    }
+
+    @Nullable
+    public NitzData getCachedNitzData() {
+        return mLatestNitzSignal != null ? mLatestNitzSignal.getValue() : null;
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelper.java b/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelper.java
new file mode 100644
index 0000000..7984c97
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelper.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import android.annotation.NonNull;
+import android.app.timedetector.PhoneTimeSuggestion;
+import android.app.timedetector.TimeDetector;
+
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+
+/**
+ * An interface to various time / time zone detection behaviors that should be centralized into
+ * new services.
+ */
+public interface NewTimeServiceHelper {
+
+    /**
+     * Suggests the time to the {@link TimeDetector}.
+     *
+     * @param suggestion the time
+     */
+    void suggestDeviceTime(@NonNull PhoneTimeSuggestion suggestion);
+
+    /**
+     * Suggests the time zone to the time zone detector.
+     *
+     * <p>NOTE: The PhoneTimeZoneSuggestion cannot be null. The zoneId it contains can be null to
+     * indicate there is no active suggestion; this can be used to clear a previous suggestion.
+     *
+     * @param suggestion the time zone
+     */
+    void maybeSuggestDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion suggestion);
+
+    /**
+     * Dumps any logs held to the supplied writer.
+     */
+    void dumpLogs(IndentingPrintWriter ipw);
+
+    /**
+     * Dumps internal state such as field values.
+     */
+    void dumpState(PrintWriter pw);
+}
diff --git a/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelperImpl.java b/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelperImpl.java
new file mode 100644
index 0000000..337423b
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/NewTimeServiceHelperImpl.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.timedetector.PhoneTimeSuggestion;
+import android.app.timedetector.TimeDetector;
+import android.content.Context;
+import android.util.LocalLog;
+import android.util.TimestampedValue;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+import com.android.internal.telephony.nitz.service.TimeZoneDetectionService;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+
+/**
+ * The real implementation of {@link NewTimeServiceHelper}.
+ */
+public final class NewTimeServiceHelperImpl implements NewTimeServiceHelper {
+
+    private final int mPhoneId;
+    private final TimeDetector mTimeDetector;
+    private final TimeZoneDetectionService mTimeZoneDetector;
+
+    private final LocalLog mTimeZoneLog = new LocalLog(30);
+    private final LocalLog mTimeLog = new LocalLog(30);
+
+    /**
+     * Records the last time zone suggestion made. Used to avoid sending duplicate suggestions to
+     * the time zone service. The value can be {@code null} to indicate no previous suggestion has
+     * been made.
+     */
+    @NonNull
+    private PhoneTimeZoneSuggestion mLastSuggestedTimeZone;
+
+    public NewTimeServiceHelperImpl(@NonNull Phone phone) {
+        mPhoneId = phone.getPhoneId();
+        Context context = Objects.requireNonNull(phone.getContext());
+        mTimeDetector = Objects.requireNonNull(context.getSystemService(TimeDetector.class));
+        mTimeZoneDetector = Objects.requireNonNull(TimeZoneDetectionService.getInstance(context));
+    }
+
+    @Override
+    public void suggestDeviceTime(@NonNull PhoneTimeSuggestion phoneTimeSuggestion) {
+        mTimeLog.log("Suggesting system clock update: " + phoneTimeSuggestion);
+
+        // 3 nullness assertions in 1 line
+        Objects.requireNonNull(phoneTimeSuggestion.getUtcTime().getValue());
+
+        TimestampedValue<Long> utcTime = phoneTimeSuggestion.getUtcTime();
+        TelephonyMetrics.getInstance().writeNITZEvent(mPhoneId, utcTime.getValue());
+        mTimeDetector.suggestPhoneTime(phoneTimeSuggestion);
+    }
+
+    @Override
+    public void maybeSuggestDeviceTimeZone(@NonNull PhoneTimeZoneSuggestion newSuggestion) {
+        Objects.requireNonNull(newSuggestion);
+
+        PhoneTimeZoneSuggestion oldSuggestion = mLastSuggestedTimeZone;
+        if (shouldSendNewTimeZoneSuggestion(oldSuggestion, newSuggestion)) {
+            mTimeZoneLog.log("Suggesting time zone update: " + newSuggestion);
+            mTimeZoneDetector.suggestPhoneTimeZone(newSuggestion);
+            mLastSuggestedTimeZone = newSuggestion;
+        }
+    }
+
+    private static boolean shouldSendNewTimeZoneSuggestion(
+            @Nullable PhoneTimeZoneSuggestion oldSuggestion,
+            @NonNull PhoneTimeZoneSuggestion newSuggestion) {
+        if (oldSuggestion == null) {
+            // No previous suggestion.
+            return true;
+        }
+        // This code relies on PhoneTimeZoneSuggestion.equals() to only check meaningful fields.
+        return !Objects.equals(newSuggestion, oldSuggestion);
+    }
+
+    @Override
+    public void dumpLogs(IndentingPrintWriter ipw) {
+        ipw.println("NewTimeServiceHelperImpl:");
+        ipw.increaseIndent();
+        ipw.println("Time Logs:");
+        ipw.increaseIndent();
+        mTimeLog.dump(ipw);
+        ipw.decreaseIndent();
+
+        ipw.println("Time zone Logs:");
+        ipw.increaseIndent();
+        mTimeZoneLog.dump(ipw);
+        ipw.decreaseIndent();
+        ipw.decreaseIndent();
+
+        // TODO Remove this line when the service moves to the system server.
+        mTimeZoneDetector.dumpLogs(ipw);
+    }
+
+    @Override
+    public void dumpState(PrintWriter pw) {
+        pw.println(" NewTimeServiceHelperImpl.mLastSuggestedTimeZone=" + mLastSuggestedTimeZone);
+        mTimeZoneDetector.dumpState(pw);
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactory.java b/src/java/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactory.java
new file mode 100644
index 0000000..58cbaaa
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactory.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.telephony.Rlog;
+import android.util.TimestampedValue;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachine.DeviceState;
+import com.android.internal.telephony.nitz.NewNitzStateMachineImpl.NitzSignalInputFilterPredicate;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A factory class for the {@link NitzSignalInputFilterPredicate} instance used by
+ * {@link NewNitzStateMachineImpl}. This class is exposed for testing and provides access to various
+ * internal components.
+ */
+@VisibleForTesting
+public final class NitzSignalInputFilterPredicateFactory {
+
+    private static final String LOG_TAG = NewNitzStateMachineImpl.LOG_TAG;
+    private static final boolean DBG = NewNitzStateMachineImpl.DBG;
+    private static final String WAKELOCK_TAG = "NitzSignalInputFilterPredicateFactory";
+
+    private NitzSignalInputFilterPredicateFactory() {}
+
+    /**
+     * Returns the real {@link NitzSignalInputFilterPredicate} to use for NITZ signal input
+     * filtering.
+     */
+    @NonNull
+    public static NitzSignalInputFilterPredicate create(
+            @NonNull Context context, @NonNull DeviceState deviceState) {
+        Objects.requireNonNull(context);
+        Objects.requireNonNull(deviceState);
+
+        TrivalentPredicate[] components = new TrivalentPredicate[] {
+                // Disables NITZ processing entirely: can return false or null.
+                createIgnoreNitzPropertyCheck(deviceState),
+                // Filters bad reference times from new signals: can return false or null.
+                createBogusElapsedRealtimeCheck(context, deviceState),
+                // Ensures oldSignal == null is always processed: can return true or null.
+                createNoOldSignalCheck(),
+                // Adds rate limiting: can return true or false.
+                createRateLimitCheck(deviceState),
+        };
+        return new NitzSignalInputFilterPredicateImpl(components);
+    }
+
+    /**
+     * A filtering function that can give a {@code true} (must process), {@code false} (must not
+     * process) and a {@code null} (no opinion) response given a previous NITZ signal and a new
+     * signal. The previous signal may be {@code null} (unless ruled out by a prior
+     * {@link TrivalentPredicate}).
+     */
+    @VisibleForTesting
+    @FunctionalInterface
+    public interface TrivalentPredicate {
+
+        /**
+         * See {@link TrivalentPredicate}.
+         */
+        @Nullable
+        Boolean mustProcessNitzSignal(
+                @Nullable TimestampedValue<NitzData> previousSignal,
+                @NonNull TimestampedValue<NitzData> newSignal);
+    }
+
+    /**
+     * Returns a {@link TrivalentPredicate} function that implements a check for the
+     * "gsm.ignore-nitz" Android system property. The function can return {@code false} or
+     * {@code null}.
+     */
+    @VisibleForTesting
+    @NonNull
+    public static TrivalentPredicate createIgnoreNitzPropertyCheck(
+            @NonNull DeviceState deviceState) {
+        return (oldSignal, newSignal) -> {
+            boolean ignoreNitz = deviceState.getIgnoreNitz();
+            if (ignoreNitz) {
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "mustProcessNitzSignal: Not processing NITZ signal because"
+                            + " gsm.ignore-nitz is set");
+                }
+                return false;
+            }
+            return null;
+        };
+    }
+
+    /**
+     * Returns a {@link TrivalentPredicate} function that implements a check for a bad reference
+     * time associated with {@code newSignal}. The function can return {@code false} or
+     * {@code null}.
+     */
+    @VisibleForTesting
+    @NonNull
+    public static TrivalentPredicate createBogusElapsedRealtimeCheck(
+            @NonNull Context context, @NonNull DeviceState deviceState) {
+        PowerManager powerManager =
+                (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        final WakeLock wakeLock =
+                powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
+
+        return (oldSignal, newSignal) -> {
+            Objects.requireNonNull(newSignal);
+
+            // Validate the newSignal to reject obviously bogus elapsedRealtime values.
+            try {
+                // Acquire the wake lock as we are reading the elapsed realtime clock below.
+                wakeLock.acquire();
+
+                long elapsedRealtime = deviceState.elapsedRealtime();
+                long millisSinceNitzReceived = elapsedRealtime - newSignal.getReferenceTimeMillis();
+                if (millisSinceNitzReceived < 0 || millisSinceNitzReceived > Integer.MAX_VALUE) {
+                    if (DBG) {
+                        Rlog.d(LOG_TAG, "mustProcessNitzSignal: Not processing NITZ signal"
+                                + " because unexpected elapsedRealtime=" + elapsedRealtime
+                                + " nitzSignal=" + newSignal);
+                    }
+                    return false;
+                }
+                return null;
+            } finally {
+                wakeLock.release();
+            }
+        };
+    }
+
+    /**
+     * Returns a {@link TrivalentPredicate} function that implements a check for a {@code null}
+     * {@code oldSignal} (indicating there's no history). The function can return {@code true}
+     * or {@code null}.
+     */
+    @VisibleForTesting
+    @NonNull
+    public static TrivalentPredicate createNoOldSignalCheck() {
+        // Always process a signal when there was no previous signal.
+        return (oldSignal, newSignal) -> oldSignal == null ? true : null;
+    }
+
+    /**
+     * Returns a {@link TrivalentPredicate} function that implements filtering using
+     * {@code oldSignal} and {@code newSignal}. The function can return {@code true} or
+     * {@code false} and so is intended as the final function in a chain.
+     *
+     * Function detail: if an NITZ signal received that is too similar to a previous one
+     * it should be disregarded if it's received within a configured time period.
+     * The general contract for {@link TrivalentPredicate} allows {@code previousSignal} to be
+     * {@code null}, but previous functions are expected to prevent it in this case.
+     */
+    @VisibleForTesting
+    @NonNull
+    public static TrivalentPredicate createRateLimitCheck(@NonNull DeviceState deviceState) {
+        return new TrivalentPredicate() {
+            @Override
+            @NonNull
+            public Boolean mustProcessNitzSignal(
+                    @NonNull TimestampedValue<NitzData> previousSignal,
+                    @NonNull TimestampedValue<NitzData> newSignal) {
+                Objects.requireNonNull(newSignal);
+                Objects.requireNonNull(newSignal.getValue());
+                Objects.requireNonNull(previousSignal);
+                Objects.requireNonNull(previousSignal.getValue());
+
+                NitzData newNitzData = newSignal.getValue();
+                NitzData previousNitzData = previousSignal.getValue();
+
+                // Compare the discrete NitzData fields associated with local time offset. Any
+                // difference and we should process the signal regardless of how recent the last one
+                // was.
+                if (!offsetInfoIsTheSame(previousNitzData, newNitzData)) {
+                    return true;
+                }
+
+                // Now check the continuous NitzData field (time) to see if it is sufficiently
+                // different.
+                int nitzUpdateSpacing = deviceState.getNitzUpdateSpacingMillis();
+                int nitzUpdateDiff = deviceState.getNitzUpdateDiffMillis();
+
+                // Calculate the elapsed time between the new signal and the last signal.
+                long elapsedRealtimeSinceLastSaved = newSignal.getReferenceTimeMillis()
+                        - previousSignal.getReferenceTimeMillis();
+
+                // Calculate the UTC difference between the time the two signals hold.
+                long utcTimeDifferenceMillis = newNitzData.getCurrentTimeInMillis()
+                        - previousNitzData.getCurrentTimeInMillis();
+
+                // Ideally the difference between elapsedRealtimeSinceLastSaved and
+                // utcTimeDifferenceMillis would be zero.
+                long millisGainedOrLost = Math
+                        .abs(utcTimeDifferenceMillis - elapsedRealtimeSinceLastSaved);
+
+                if (elapsedRealtimeSinceLastSaved > nitzUpdateSpacing
+                        || millisGainedOrLost > nitzUpdateDiff) {
+                    return true;
+                }
+
+                if (DBG) {
+                    Rlog.d(LOG_TAG, "mustProcessNitzSignal: NITZ signal filtered"
+                            + " previousSignal=" + previousSignal
+                            + ", newSignal=" + newSignal
+                            + ", nitzUpdateSpacing=" + nitzUpdateSpacing
+                            + ", nitzUpdateDiff=" + nitzUpdateDiff);
+                }
+                return false;
+            }
+
+            private boolean offsetInfoIsTheSame(NitzData one, NitzData two) {
+                return Objects.equals(two.getDstAdjustmentMillis(), one.getDstAdjustmentMillis())
+                        && Objects.equals(
+                                two.getEmulatorHostTimeZone(), one.getEmulatorHostTimeZone())
+                        && two.getLocalOffsetMillis() == one.getLocalOffsetMillis();
+            }
+        };
+    }
+
+    /**
+     * An implementation of {@link NitzSignalInputFilterPredicate} that tries a series of
+     * {@link TrivalentPredicate} instances until one provides a {@code true} or {@code false}
+     * response indicating that the {@code newSignal} should be processed or not. If all return
+     * {@code null} then a default of {@code true} is returned.
+     */
+    @VisibleForTesting
+    public static class NitzSignalInputFilterPredicateImpl
+            implements NitzSignalInputFilterPredicate {
+
+        @NonNull
+        private final TrivalentPredicate[] mComponents;
+
+        @VisibleForTesting
+        public NitzSignalInputFilterPredicateImpl(@NonNull TrivalentPredicate[] components) {
+            this.mComponents = Arrays.copyOf(components, components.length);
+        }
+
+        @Override
+        public boolean mustProcessNitzSignal(@Nullable TimestampedValue<NitzData> oldSignal,
+                @NonNull TimestampedValue<NitzData> newSignal) {
+            Objects.requireNonNull(newSignal);
+
+            for (TrivalentPredicate component : mComponents) {
+                Boolean result = component.mustProcessNitzSignal(oldSignal, newSignal);
+                if (result != null) {
+                    return result;
+                }
+            }
+            // The default is to process.
+            return true;
+        }
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/TimeZoneSuggesterImpl.java b/src/java/com/android/internal/telephony/nitz/TimeZoneSuggesterImpl.java
new file mode 100644
index 0000000..c5d9df6
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/TimeZoneSuggesterImpl.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import static com.android.internal.telephony.TimeZoneLookupHelper.CountryResult.QUALITY_DEFAULT_BOOSTED;
+import static com.android.internal.telephony.TimeZoneLookupHelper.CountryResult.QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS;
+import static com.android.internal.telephony.TimeZoneLookupHelper.CountryResult.QUALITY_MULTIPLE_ZONES_SAME_OFFSET;
+import static com.android.internal.telephony.TimeZoneLookupHelper.CountryResult.QUALITY_SINGLE_ZONE;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.createEmptySuggestion;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import android.util.TimestampedValue;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachine.DeviceState;
+import com.android.internal.telephony.TimeZoneLookupHelper;
+import com.android.internal.telephony.TimeZoneLookupHelper.CountryResult;
+import com.android.internal.telephony.nitz.NewNitzStateMachineImpl.TimeZoneSuggester;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+
+import java.util.Objects;
+
+/**
+ * The real implementation of {@link TimeZoneSuggester}.
+ */
+@VisibleForTesting
+public class TimeZoneSuggesterImpl implements TimeZoneSuggester {
+
+    private static final String LOG_TAG = NewNitzStateMachineImpl.LOG_TAG;
+
+    private final DeviceState mDeviceState;
+    private final TimeZoneLookupHelper mTimeZoneLookupHelper;
+
+    @VisibleForTesting
+    public TimeZoneSuggesterImpl(
+            @NonNull DeviceState deviceState, @NonNull TimeZoneLookupHelper timeZoneLookupHelper) {
+        mDeviceState = Objects.requireNonNull(deviceState);
+        mTimeZoneLookupHelper = Objects.requireNonNull(timeZoneLookupHelper);
+    }
+
+    @Override
+    @NonNull
+    public PhoneTimeZoneSuggestion getTimeZoneSuggestion(int phoneId,
+            @Nullable String countryIsoCode, @Nullable TimestampedValue<NitzData> nitzSignal) {
+        try {
+            // Check for overriding NITZ-based signals from Android running in an emulator.
+            PhoneTimeZoneSuggestion overridingSuggestion = null;
+            if (nitzSignal != null) {
+                NitzData nitzData = nitzSignal.getValue();
+                if (nitzData.getEmulatorHostTimeZone() != null) {
+                    overridingSuggestion = new PhoneTimeZoneSuggestion(phoneId);
+                    overridingSuggestion.setZoneId(nitzData.getEmulatorHostTimeZone().getID());
+                    overridingSuggestion.setMatchType(PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID);
+                    overridingSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+                    overridingSuggestion.addDebugInfo("Emulator time zone override: " + nitzData);
+                }
+            }
+
+            PhoneTimeZoneSuggestion suggestion;
+            if (overridingSuggestion != null) {
+                suggestion = overridingSuggestion;
+            } else if (countryIsoCode == null) {
+                if (nitzSignal == null) {
+                    suggestion = createEmptySuggestion(phoneId,
+                            "getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=null");
+                } else {
+                    // NITZ only - wait until we have a country.
+                    suggestion = createEmptySuggestion(phoneId, "getTimeZoneSuggestion:"
+                            + " nitzSignal=" + nitzSignal + ", countryIsoCode=null");
+                }
+            } else { // countryIsoCode != null
+                if (nitzSignal == null) {
+                    if (countryIsoCode.isEmpty()) {
+                        // This is assumed to be a test network with no NITZ data to go on.
+                        suggestion = createEmptySuggestion(phoneId,
+                                "getTimeZoneSuggestion: nitzSignal=null, countryIsoCode=\"\"");
+                    } else {
+                        // Country only
+                        suggestion = findTimeZoneFromNetworkCountryCode(
+                                phoneId, countryIsoCode, mDeviceState.currentTimeMillis());
+                    }
+                } else { // nitzSignal != null
+                    if (countryIsoCode.isEmpty()) {
+                        // We have been told we have a country code but it's empty. This is most
+                        // likely because we're on a test network that's using a bogus MCC
+                        // (eg, "001"). Obtain a TimeZone based only on the NITZ parameters: without
+                        // a country it will be arbitrary, but it should at least have the correct
+                        // offset.
+                        suggestion = findTimeZoneForTestNetwork(phoneId, nitzSignal);
+                    } else {
+                        // We have both NITZ and Country code.
+                        suggestion = findTimeZoneFromCountryAndNitz(
+                                phoneId, countryIsoCode, nitzSignal);
+                    }
+                }
+            }
+
+            // Ensure the return value is never null.
+            Objects.requireNonNull(suggestion);
+
+            return suggestion;
+        } catch (RuntimeException e) {
+            // This would suggest a coding error. Log at a high level and try to avoid leaving the
+            // device in a bad state by making an "empty" suggestion.
+            String message = "getTimeZoneSuggestion: Error during lookup: "
+                    + " countryIsoCode=" + countryIsoCode
+                    + ", nitzSignal=" + nitzSignal
+                    + ", e=" + e.getMessage();
+            PhoneTimeZoneSuggestion errorSuggestion = createEmptySuggestion(phoneId, message);
+            errorSuggestion.addDebugInfo(message);
+            Rlog.w(LOG_TAG, message, e);
+            return errorSuggestion;
+        }
+    }
+
+    /**
+     * Creates a {@link PhoneTimeZoneSuggestion} using only NITZ. This happens when the device
+     * is attached to a test cell with an unrecognized MCC. In these cases we try to return a
+     * suggestion for an arbitrary time zone that matches the NITZ offset information.
+     */
+    @NonNull
+    private PhoneTimeZoneSuggestion findTimeZoneForTestNetwork(
+            int phoneId, @NonNull TimestampedValue<NitzData> nitzSignal) {
+        Objects.requireNonNull(nitzSignal);
+        NitzData nitzData = Objects.requireNonNull(nitzSignal.getValue());
+
+        PhoneTimeZoneSuggestion result = new PhoneTimeZoneSuggestion(phoneId);
+        result.addDebugInfo("findTimeZoneForTestNetwork: nitzSignal=" + nitzSignal);
+        TimeZoneLookupHelper.OffsetResult lookupResult =
+                mTimeZoneLookupHelper.lookupByNitz(nitzData);
+        if (lookupResult == null) {
+            result.addDebugInfo("findTimeZoneForTestNetwork: No zone found");
+        } else {
+            result.setZoneId(lookupResult.getTimeZone().getID());
+            result.setMatchType(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY);
+            int quality = lookupResult.getIsOnlyMatch() ? PhoneTimeZoneSuggestion.SINGLE_ZONE
+                    : PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET;
+            result.setQuality(quality);
+            result.addDebugInfo("findTimeZoneForTestNetwork: lookupResult=" + lookupResult);
+        }
+        return result;
+    }
+
+    /**
+     * Creates a {@link PhoneTimeZoneSuggestion} using network country code and NITZ.
+     */
+    @NonNull
+    private PhoneTimeZoneSuggestion findTimeZoneFromCountryAndNitz(
+            int phoneId, @NonNull String countryIsoCode,
+            @NonNull TimestampedValue<NitzData> nitzSignal) {
+        Objects.requireNonNull(countryIsoCode);
+        Objects.requireNonNull(nitzSignal);
+
+        PhoneTimeZoneSuggestion suggestion = new PhoneTimeZoneSuggestion(phoneId);
+        suggestion.addDebugInfo("findTimeZoneFromCountryAndNitz: countryIsoCode=" + countryIsoCode
+                + ", nitzSignal=" + nitzSignal);
+        NitzData nitzData = Objects.requireNonNull(nitzSignal.getValue());
+        if (isNitzSignalOffsetInfoBogus(countryIsoCode, nitzData)) {
+            suggestion.addDebugInfo("findTimeZoneFromCountryAndNitz: NITZ signal looks bogus");
+            return suggestion;
+        }
+
+        // Try to find a match using both country + NITZ signal.
+        TimeZoneLookupHelper.OffsetResult lookupResult =
+                mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, countryIsoCode);
+        if (lookupResult != null) {
+            suggestion.setZoneId(lookupResult.getTimeZone().getID());
+            suggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            int quality = lookupResult.getIsOnlyMatch()
+                    ? PhoneTimeZoneSuggestion.SINGLE_ZONE
+                    : PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET;
+            suggestion.setQuality(quality);
+            suggestion.addDebugInfo("findTimeZoneFromCountryAndNitz: lookupResult=" + lookupResult);
+            return suggestion;
+        }
+
+        // The country + offset provided no match, so see if the country by itself would be enough.
+        CountryResult countryResult = mTimeZoneLookupHelper.lookupByCountry(
+                countryIsoCode, nitzData.getCurrentTimeInMillis());
+        if (countryResult == null) {
+            // Country not recognized.
+            suggestion.addDebugInfo(
+                    "findTimeZoneFromCountryAndNitz: lookupByCountry() country not recognized");
+            return suggestion;
+        }
+
+        // If the country has a single zone, or it has multiple zones but the default zone is
+        // "boosted" (i.e. the country default is considered a good suggestion in most cases) then
+        // use it.
+        if (countryResult.quality == QUALITY_SINGLE_ZONE
+                || countryResult.quality == QUALITY_DEFAULT_BOOSTED) {
+            suggestion.setZoneId(countryResult.zoneId);
+            suggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            suggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            suggestion.addDebugInfo(
+                    "findTimeZoneFromCountryAndNitz: high quality country-only suggestion:"
+                            + " countryResult=" + countryResult);
+            return suggestion;
+        }
+
+        // Quality is not high enough to set the zone using country only.
+        suggestion.addDebugInfo("findTimeZoneFromCountryAndNitz: country-only suggestion quality"
+                + " not high enough. countryResult=" + countryResult);
+        return suggestion;
+    }
+
+    /**
+     * Creates a {@link PhoneTimeZoneSuggestion} using only network country code; works well on
+     * countries which only have one time zone or multiple zones with the same offset.
+     *
+     * @param countryIsoCode country code from network MCC
+     * @param whenMillis the time to use when looking at time zone rules data
+     */
+    @NonNull
+    private PhoneTimeZoneSuggestion findTimeZoneFromNetworkCountryCode(
+            int phoneId, @NonNull String countryIsoCode, long whenMillis) {
+        Objects.requireNonNull(countryIsoCode);
+        if (TextUtils.isEmpty(countryIsoCode)) {
+            throw new IllegalArgumentException("countryIsoCode must not be empty");
+        }
+
+        PhoneTimeZoneSuggestion result = new PhoneTimeZoneSuggestion(phoneId);
+        result.addDebugInfo("findTimeZoneFromNetworkCountryCode:"
+                + " whenMillis=" + whenMillis + ", countryIsoCode=" + countryIsoCode);
+        CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
+                countryIsoCode, whenMillis);
+        if (lookupResult != null) {
+            result.setZoneId(lookupResult.zoneId);
+            result.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+
+            int quality;
+            if (lookupResult.quality == QUALITY_SINGLE_ZONE
+                    || lookupResult.quality == QUALITY_DEFAULT_BOOSTED) {
+                quality = PhoneTimeZoneSuggestion.SINGLE_ZONE;
+            } else if (lookupResult.quality == QUALITY_MULTIPLE_ZONES_SAME_OFFSET) {
+                quality = PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET;
+            } else if (lookupResult.quality == QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS) {
+                quality = PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
+            } else {
+                // This should never happen.
+                throw new IllegalArgumentException(
+                        "lookupResult.quality not recognized: countryIsoCode=" + countryIsoCode
+                                + ", whenMillis=" + whenMillis + ", lookupResult=" + lookupResult);
+            }
+            result.setQuality(quality);
+            result.addDebugInfo("findTimeZoneFromNetworkCountryCode: lookupResult=" + lookupResult);
+        } else {
+            result.addDebugInfo("findTimeZoneFromNetworkCountryCode: Country not recognized?");
+        }
+        return result;
+    }
+
+    /**
+     * Returns true if the NITZ signal is definitely bogus, assuming that the country is correct.
+     */
+    private boolean isNitzSignalOffsetInfoBogus(String countryIsoCode, NitzData nitzData) {
+        if (TextUtils.isEmpty(countryIsoCode)) {
+            // We cannot say for sure.
+            return false;
+        }
+
+        boolean zeroOffsetNitz = nitzData.getLocalOffsetMillis() == 0;
+        return zeroOffsetNitz && !countryUsesUtc(countryIsoCode, nitzData);
+    }
+
+    private boolean countryUsesUtc(String countryIsoCode, NitzData nitzData) {
+        return mTimeZoneLookupHelper.countryUsesUtc(
+                countryIsoCode, nitzData.getCurrentTimeInMillis());
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestion.java b/src/java/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestion.java
new file mode 100644
index 0000000..a5078a7
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestion.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz.service;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A suggested time zone from a Phone-based signal, e.g. from MCC and NITZ information.
+ */
+public final class PhoneTimeZoneSuggestion implements Parcelable {
+
+    public static final Creator<PhoneTimeZoneSuggestion> CREATOR =
+            new Creator<PhoneTimeZoneSuggestion>() {
+                public PhoneTimeZoneSuggestion createFromParcel(Parcel in) {
+                    return PhoneTimeZoneSuggestion.createFromParcel(in);
+                }
+
+                public PhoneTimeZoneSuggestion[] newArray(int size) {
+                    return new PhoneTimeZoneSuggestion[size];
+                }
+            };
+
+    /**
+     * Creates an empty time zone suggestion, i.e. one that will cancel previous suggestions with
+     * the same {@code phoneId}.
+     */
+    @NonNull
+    public static PhoneTimeZoneSuggestion createEmptySuggestion(
+            int phoneId, @NonNull String debugInfo) {
+        PhoneTimeZoneSuggestion timeZoneSuggestion = new PhoneTimeZoneSuggestion(phoneId);
+        timeZoneSuggestion.addDebugInfo(debugInfo);
+        return timeZoneSuggestion;
+    }
+
+    @IntDef({ MATCH_TYPE_NA, NETWORK_COUNTRY_ONLY, NETWORK_COUNTRY_AND_OFFSET, EMULATOR_ZONE_ID,
+            TEST_NETWORK_OFFSET_ONLY })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MatchType {}
+
+    /** Used when match type is not applicable. */
+    public static final int MATCH_TYPE_NA = 0;
+
+    /**
+     * Only the network country is known.
+     */
+    public static final int NETWORK_COUNTRY_ONLY = 2;
+
+    /**
+     * Both the network county and offset were known.
+     */
+    public static final int NETWORK_COUNTRY_AND_OFFSET = 3;
+
+    /**
+     * The device is running in an emulator and an NITZ signal was simulated containing an
+     * Android extension with an explicit Olson ID.
+     */
+    public static final int EMULATOR_ZONE_ID = 4;
+
+    /**
+     * The phone is most likely running in a test network not associated with a country (this is
+     * distinct from the country just not being known yet).
+     * Historically, Android has just picked an arbitrary time zone with the correct offset when
+     * on a test network.
+     */
+    public static final int TEST_NETWORK_OFFSET_ONLY = 5;
+
+    @IntDef({ QUALITY_NA, SINGLE_ZONE, MULTIPLE_ZONES_WITH_SAME_OFFSET,
+            MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Quality {}
+
+    /** Used when quality is not applicable. */
+    public static final int QUALITY_NA = 0;
+
+    /** There is only one answer */
+    public static final int SINGLE_ZONE = 1;
+
+    /**
+     * There are multiple answers, but they all shared the same offset / DST state at the time
+     * the suggestion was created. i.e. it might be the wrong zone but the user won't notice
+     * immediately if it is wrong.
+     */
+    public static final int MULTIPLE_ZONES_WITH_SAME_OFFSET = 2;
+
+    /**
+     * There are multiple answers with different offsets. The one given is just one possible.
+     */
+    public static final int MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS = 3;
+
+    /**
+     * The ID of the phone this suggestion is associated with. For multiple-sim devices this
+     * helps to establish origin so filtering / stickiness can be implemented.
+     */
+    private final int mPhoneId;
+
+    /**
+     * The suggestion. {@code null} means there is no current suggestion and any previous suggestion
+     * should be forgotten.
+     */
+    private String mZoneId;
+
+    /**
+     * The type of "match" used to establish the time zone.
+     */
+    @MatchType
+    private int mMatchType;
+
+    /**
+     * A measure of the quality of the time zone suggestion, i.e. how confident one could be in
+     * it.
+     */
+    @Quality
+    private int mQuality;
+
+    /**
+     * Free-form debug information about how the signal was derived. Used for debug only,
+     * intentionally not used in equals(), etc.
+     */
+    private List<String> mDebugInfo;
+
+    public PhoneTimeZoneSuggestion(int phoneId) {
+        this.mPhoneId = phoneId;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static PhoneTimeZoneSuggestion createFromParcel(Parcel in) {
+        int phoneId = in.readInt();
+        PhoneTimeZoneSuggestion phoneTimeZoneSuggestion = new PhoneTimeZoneSuggestion(phoneId);
+        phoneTimeZoneSuggestion.mZoneId = in.readString();
+        phoneTimeZoneSuggestion.mMatchType = in.readInt();
+        phoneTimeZoneSuggestion.mQuality = in.readInt();
+        phoneTimeZoneSuggestion.mDebugInfo =
+                (List<String>) in.readArrayList(PhoneTimeZoneSuggestion.class.getClassLoader());
+        return phoneTimeZoneSuggestion;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mPhoneId);
+        dest.writeString(mZoneId);
+        dest.writeInt(mMatchType);
+        dest.writeInt(mQuality);
+        dest.writeList(mDebugInfo);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public int getPhoneId() {
+        return mPhoneId;
+    }
+
+    @Nullable
+    public String getZoneId() {
+        return mZoneId;
+    }
+
+    public void setZoneId(@Nullable String zoneId) {
+        this.mZoneId = zoneId;
+    }
+
+
+    @MatchType
+    public int getMatchType() {
+        return mMatchType;
+    }
+
+    public void setMatchType(@MatchType int matchType) {
+        this.mMatchType = matchType;
+    }
+    @Quality
+    public int getQuality() {
+        return mQuality;
+    }
+
+    public void setQuality(@Quality int quality) {
+        this.mQuality = quality;
+    }
+
+    public List<String> getDebugInfo() {
+        return Collections.unmodifiableList(mDebugInfo);
+    }
+
+    /**
+     * Associates information with the instance that can be useful for debugging / logging. The
+     * information is present in {@link #toString()} but is not considered for
+     * {@link #equals(Object)} and {@link #hashCode()}.
+     */
+    public void addDebugInfo(String... debugInfos) {
+        if (mDebugInfo == null) {
+            mDebugInfo = new ArrayList<>();
+        }
+        mDebugInfo.addAll(Arrays.asList(debugInfos));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        PhoneTimeZoneSuggestion that = (PhoneTimeZoneSuggestion) o;
+        return mPhoneId == that.mPhoneId
+                && mMatchType == that.mMatchType
+                && mQuality == that.mQuality
+                && Objects.equals(mZoneId, that.mZoneId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPhoneId, mZoneId, mMatchType, mQuality);
+    }
+
+    @Override
+    public String toString() {
+        return "PhoneTimeZoneSuggestion{"
+                + "mPhoneId=" + mPhoneId
+                + ", mZoneId='" + mZoneId + '\''
+                + ", mMatchType=" + mMatchType
+                + ", mQuality=" + mQuality
+                + ", mDebugInfo=" + mDebugInfo
+                + '}';
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionService.java b/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionService.java
new file mode 100644
index 0000000..a766d9a
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionService.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz.service;
+
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MATCH_TYPE_NA;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.QUALITY_NA;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.SINGLE_ZONE;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.Objects;
+
+/**
+ * A singleton, stateful time zone detection service that is aware of multiple phone devices. It
+ * keeps track of the most recent suggestion from each phone and it uses the best based on a scoring
+ * algorithm. If both phones provide the same score then the phone with the lowest numeric ID
+ * "wins". If the situation changes and it is no longer possible to be confident about the time
+ * zone, phones must submit an empty suggestion in order to "withdraw" their previous suggestion.
+ *
+ * <p>Ultimately, this responsibility will be moved to system server and then it will be extended /
+ * rewritten to handle non-telephony time zone signals.
+ */
+public class TimeZoneDetectionService {
+
+    /**
+     * Used by {@link TimeZoneDetectionService} to interact with device settings. It can be faked
+     * for tests.
+     */
+    @VisibleForTesting
+    public interface Helper {
+
+        /**
+         * Callback interface for automatic detection enable/disable changes.
+         */
+        interface Listener {
+            /**
+             * Automatic time zone detection has been enabled or disabled.
+             */
+            void onTimeZoneDetectionChange(boolean enabled);
+        }
+
+        /**
+         * Sets a listener that will be called when the automatic time / time zone detection setting
+         * changes.
+         */
+        void setListener(Listener listener);
+
+        /**
+         * Returns true if automatic time zone detection is enabled in settings.
+         */
+        boolean isTimeZoneDetectionEnabled();
+
+        /**
+         * Returns true if the device has had an explicit time zone set.
+         */
+        boolean isTimeZoneSettingInitialized();
+
+        /**
+         * Set the device time zone from the suggestion as needed.
+         */
+        void setDeviceTimeZoneFromSuggestion(@NonNull PhoneTimeZoneSuggestion timeZoneSuggestion);
+
+        /**
+         * Dumps any logs held to the supplied writer.
+         */
+        void dumpLogs(IndentingPrintWriter ipw);
+
+        /**
+         * Dumps internal state such as field values.
+         */
+        void dumpState(PrintWriter pw);
+    }
+
+    static final String LOG_TAG = "TimeZoneDetectionService";
+    static final boolean DBG = true;
+
+    /**
+     * The abstract score for an empty or invalid suggestion.
+     *
+     * Used to score suggestions where there is no zone.
+     */
+    @VisibleForTesting
+    public static final int SCORE_NONE = 0;
+
+    /**
+     * The abstract score for a low quality suggestion.
+     *
+     * Used to score suggestions where:
+     * The suggested zone ID is one of several possibilities, and the possibilities have different
+     * offsets.
+     *
+     * You would have to be quite desperate to want to use this choice.
+     */
+    @VisibleForTesting
+    public static final int SCORE_LOW = 1;
+
+    /**
+     * The abstract score for a medium quality suggestion.
+     *
+     * Used for:
+     * The suggested zone ID is one of several possibilities but at least the possibilities have the
+     * same offset. Users would get the correct time but for the wrong reason. i.e. their device may
+     * switch to DST at the wrong time and (for example) their calendar events.
+     */
+    @VisibleForTesting
+    public static final int SCORE_MEDIUM = 2;
+
+    /**
+     * The abstract score for a high quality suggestion.
+     *
+     * Used for:
+     * The suggestion was for one zone ID and the answer was unambiguous and likely correct given
+     * the info available.
+     */
+    @VisibleForTesting
+    public static final int SCORE_HIGH = 3;
+
+    /**
+     * The abstract score for a highest quality suggestion.
+     *
+     * Used for:
+     * Suggestions that must "win" because they constitute test or emulator zone ID.
+     */
+    @VisibleForTesting
+    public static final int SCORE_HIGHEST = 4;
+
+    /** The threshold at which suggestions are good enough to use to set the device's time zone. */
+    @VisibleForTesting
+    public static final int SCORE_USAGE_THRESHOLD = SCORE_MEDIUM;
+
+    /** The singleton instance. */
+    private static TimeZoneDetectionService sInstance;
+
+    /**
+     * Returns the singleton instance, constructing as needed with the supplied context.
+     */
+    public static synchronized TimeZoneDetectionService getInstance(Context context) {
+        if (sInstance == null) {
+            Helper timeZoneDetectionServiceHelper = new TimeZoneDetectionServiceHelperImpl(context);
+            sInstance = new TimeZoneDetectionService(timeZoneDetectionServiceHelper);
+        }
+        return sInstance;
+    }
+
+    private static final int KEEP_SUGGESTION_HISTORY_SIZE = 30;
+
+    /**
+     * A mapping from phoneId to a linked list of time zone suggestions (the head being the latest).
+     * We typically expect one or two entries in this Map: devices will have a small number
+     * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with
+     * the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size.
+     */
+    @GuardedBy("this")
+    private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId =
+            new ArrayMap<>();
+
+    /**
+     * The most recent best guess of time zone from all phones. Can be {@code null} to indicate
+     * there would be no current suggestion.
+     */
+    @GuardedBy("this")
+    @Nullable
+    private QualifiedPhoneTimeZoneSuggestion mCurrentSuggestion;
+
+    // Dependencies and log state.
+    private final Helper mTimeZoneDetectionServiceHelper;
+
+    @VisibleForTesting
+    public TimeZoneDetectionService(Helper timeZoneDetectionServiceHelper) {
+        mTimeZoneDetectionServiceHelper = timeZoneDetectionServiceHelper;
+        mTimeZoneDetectionServiceHelper.setListener(enabled -> {
+            if (enabled) {
+                handleAutoTimeZoneEnabled();
+            }
+        });
+    }
+
+    /**
+     * Suggests a time zone for the device, or withdraws a previous suggestion if
+     * {@link PhoneTimeZoneSuggestion#getZoneId()} is {@code null}. The suggestion is scoped to a
+     * specific {@link PhoneTimeZoneSuggestion#getPhoneId() phone}.
+     * See {@link PhoneTimeZoneSuggestion} for an explanation of the metadata associated with a
+     * suggestion. The service uses suggestions to decide whether to modify the device's time zone
+     * setting and what to set it to.
+     */
+    public synchronized void suggestPhoneTimeZone(@NonNull PhoneTimeZoneSuggestion newSuggestion) {
+        if (DBG) {
+            Log.d(LOG_TAG, "suggestPhoneTimeZone: newSuggestion=" + newSuggestion);
+        }
+        Objects.requireNonNull(newSuggestion);
+
+        int score = scoreSuggestion(newSuggestion);
+        QualifiedPhoneTimeZoneSuggestion scoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(newSuggestion, score);
+
+        // Record the suggestion against the correct phoneId.
+        LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
+                mSuggestionByPhoneId.get(newSuggestion.getPhoneId());
+        if (suggestions == null) {
+            suggestions = new LinkedList<>();
+            mSuggestionByPhoneId.put(newSuggestion.getPhoneId(), suggestions);
+        }
+        suggestions.addFirst(scoredSuggestion);
+        if (suggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) {
+            suggestions.removeLast();
+        }
+
+        // Now run the competition between the phones' suggestions.
+        doTimeZoneDetection();
+    }
+
+    private static int scoreSuggestion(@NonNull PhoneTimeZoneSuggestion suggestion) {
+        int score;
+        if (suggestion.getZoneId() == null || !isValid(suggestion)) {
+            score = SCORE_NONE;
+        } else if (suggestion.getMatchType() == TEST_NETWORK_OFFSET_ONLY
+                || suggestion.getMatchType() == EMULATOR_ZONE_ID) {
+            // Handle emulator / test cases : These suggestions should always just be used.
+            score = SCORE_HIGHEST;
+        } else if (suggestion.getQuality() == SINGLE_ZONE) {
+            score = SCORE_HIGH;
+        } else if (suggestion.getQuality() == MULTIPLE_ZONES_WITH_SAME_OFFSET) {
+            // The suggestion may be wrong, but at least the offset should be correct.
+            score = SCORE_MEDIUM;
+        } else if (suggestion.getQuality() == MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS) {
+            // The suggestion has a good chance of being wrong.
+            score = SCORE_LOW;
+        } else {
+            throw new AssertionError();
+        }
+        return score;
+    }
+
+    private static boolean isValid(@NonNull PhoneTimeZoneSuggestion suggestion) {
+        int quality = suggestion.getQuality();
+        int matchType = suggestion.getMatchType();
+        if (suggestion.getZoneId() == null) {
+            return quality == QUALITY_NA && matchType == MATCH_TYPE_NA;
+        } else {
+            boolean qualityValid = quality == SINGLE_ZONE
+                    || quality == MULTIPLE_ZONES_WITH_SAME_OFFSET
+                    || quality == MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
+            boolean matchTypeValid = matchType == NETWORK_COUNTRY_ONLY
+                    || matchType == NETWORK_COUNTRY_AND_OFFSET
+                    || matchType == EMULATOR_ZONE_ID
+                    || matchType == TEST_NETWORK_OFFSET_ONLY;
+            return qualityValid && matchTypeValid;
+        }
+    }
+
+    /**
+     * Finds the best available time zone suggestion from all phones. If it is high-enough quality
+     * and automatic time zone detection is enabled then it will be set on the device. The outcome
+     * can be that this service becomes / remains un-opinionated and nothing is set.
+     */
+    @GuardedBy("this")
+    private void doTimeZoneDetection() {
+        QualifiedPhoneTimeZoneSuggestion bestSuggestion = findBestSuggestion();
+        boolean timeZoneDetectionEnabled =
+                mTimeZoneDetectionServiceHelper.isTimeZoneDetectionEnabled();
+
+        // Work out what to do with the best suggestion.
+        if (bestSuggestion == null) {
+            // There is no suggestion. Become un-opinionated.
+            if (DBG) {
+                Log.d(LOG_TAG, "doTimeZoneDetection: No good suggestion."
+                        + " bestSuggestion=null"
+                        + ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
+            }
+            mCurrentSuggestion = null;
+            return;
+        }
+
+        // Special case handling for uninitialized devices. This should only happen once.
+        String newZoneId = bestSuggestion.suggestion.getZoneId();
+        if (newZoneId != null && !mTimeZoneDetectionServiceHelper.isTimeZoneSettingInitialized()) {
+            Log.i(LOG_TAG, "doTimeZoneDetection: Device has no time zone set so might set the"
+                    + " device to the best available suggestion."
+                    + " bestSuggestion=" + bestSuggestion
+                    + ", timeZoneDetectionEnabled=" + timeZoneDetectionEnabled);
+
+            mCurrentSuggestion = bestSuggestion;
+            if (timeZoneDetectionEnabled) {
+                mTimeZoneDetectionServiceHelper.setDeviceTimeZoneFromSuggestion(
+                        bestSuggestion.suggestion);
+            }
+            return;
+        }
+
+        boolean suggestionGoodEnough = bestSuggestion.score >= SCORE_USAGE_THRESHOLD;
+        if (!suggestionGoodEnough) {
+            if (DBG) {
+                Log.d(LOG_TAG, "doTimeZoneDetection: Suggestion not good enough."
+                        + " bestSuggestion=" + bestSuggestion);
+            }
+            mCurrentSuggestion = null;
+            return;
+        }
+
+        // Paranoia: Every suggestion above the SCORE_USAGE_THRESHOLD should have a non-null time
+        // zone ID.
+        if (newZoneId == null) {
+            Log.w(LOG_TAG, "Empty zone suggestion scored higher than expected. This is an error:"
+                    + " bestSuggestion=" + bestSuggestion);
+            mCurrentSuggestion = null;
+            return;
+        }
+
+        // There is a good suggestion. Store the suggestion and set the device time zone if
+        // settings allow.
+        mCurrentSuggestion = bestSuggestion;
+
+        // Only set the device time zone if time zone detection is enabled.
+        if (!timeZoneDetectionEnabled) {
+            if (DBG) {
+                Log.d(LOG_TAG, "doTimeZoneDetection: Not setting the time zone because time zone"
+                        + " detection is disabled."
+                        + " bestSuggestion=" + bestSuggestion);
+            }
+            return;
+        }
+        mTimeZoneDetectionServiceHelper.setDeviceTimeZoneFromSuggestion(bestSuggestion.suggestion);
+    }
+
+    @GuardedBy("this")
+    @Nullable
+    private QualifiedPhoneTimeZoneSuggestion findBestSuggestion() {
+        QualifiedPhoneTimeZoneSuggestion bestSuggestion = null;
+
+        // Iterate over the latest QualifiedPhoneTimeZoneSuggestion objects received for each phone
+        // and find the best. Note that we deliberately do not look at age: the caller can
+        // rate-limit so age is not a strong indicator of confidence. Instead, the callers are
+        // expected to withdraw suggestions they no longer have confidence in.
+        for (int i = 0; i < mSuggestionByPhoneId.size(); i++) {
+            LinkedList<QualifiedPhoneTimeZoneSuggestion> phoneSuggestions =
+                    mSuggestionByPhoneId.valueAt(i);
+            if (phoneSuggestions == null) {
+                // Unexpected
+                continue;
+            }
+            QualifiedPhoneTimeZoneSuggestion candidateSuggestion = phoneSuggestions.getFirst();
+            if (candidateSuggestion == null) {
+                // Unexpected
+                continue;
+            }
+
+            if (bestSuggestion == null) {
+                bestSuggestion = candidateSuggestion;
+            } else if (candidateSuggestion.score > bestSuggestion.score) {
+                bestSuggestion = candidateSuggestion;
+            } else if (candidateSuggestion.score == bestSuggestion.score) {
+                // Tie! Use the suggestion with the lowest phoneId.
+                int candidatePhoneId = candidateSuggestion.suggestion.getPhoneId();
+                int bestPhoneId = bestSuggestion.suggestion.getPhoneId();
+                if (candidatePhoneId < bestPhoneId) {
+                    bestSuggestion = candidateSuggestion;
+                }
+            }
+        }
+        return bestSuggestion;
+    }
+
+    /**
+     * Returns the current best suggestion. Not intended for general use: it is used during tests
+     * to check service behavior.
+     */
+    @VisibleForTesting
+    @Nullable
+    public synchronized QualifiedPhoneTimeZoneSuggestion findBestSuggestionForTests() {
+        return findBestSuggestion();
+    }
+
+    private synchronized void handleAutoTimeZoneEnabled() {
+        if (DBG) {
+            Log.d(LOG_TAG, "handleAutoTimeEnabled() called");
+        }
+        // When the user enabled time zone detection, run the time zone detection and change the
+        // device time zone if possible.
+        doTimeZoneDetection();
+    }
+
+    /**
+     * Dumps any logs held to the supplied writer.
+     */
+    public void dumpLogs(IndentingPrintWriter ipw) {
+        mTimeZoneDetectionServiceHelper.dumpLogs(ipw);
+    }
+
+    /**
+     * Dumps internal state such as field values.
+     */
+    public void dumpState(PrintWriter pw) {
+        pw.println(" TimeZoneDetectionService.mCurrentSuggestion=" + mCurrentSuggestion);
+        pw.println(" TimeZoneDetectionService.mSuggestionsByPhoneId=" + mSuggestionByPhoneId);
+        mTimeZoneDetectionServiceHelper.dumpState(pw);
+        pw.flush();
+    }
+
+    /**
+     * A method used to inspect service state during tests. Not intended for general use.
+     */
+    @VisibleForTesting
+    public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) {
+        LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions =
+                mSuggestionByPhoneId.get(phoneId);
+        if (suggestions == null) {
+            return null;
+        }
+        return suggestions.getFirst();
+    }
+
+    /**
+     * A {@link PhoneTimeZoneSuggestion} with additional qualifying metadata.
+     */
+    @VisibleForTesting
+    public static class QualifiedPhoneTimeZoneSuggestion {
+
+        @VisibleForTesting
+        public final PhoneTimeZoneSuggestion suggestion;
+
+        /**
+         * The score the suggestion has been given. This can be used to rank against other
+         * suggestions of the same type.
+         */
+        @VisibleForTesting
+        public final int score;
+
+        @VisibleForTesting
+        public QualifiedPhoneTimeZoneSuggestion(PhoneTimeZoneSuggestion suggestion, int score) {
+            this.suggestion = suggestion;
+            this.score = score;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            QualifiedPhoneTimeZoneSuggestion that = (QualifiedPhoneTimeZoneSuggestion) o;
+            return score == that.score
+                    && suggestion.equals(that.suggestion);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(score, suggestion);
+        }
+
+        @Override
+        public String toString() {
+            return "QualifiedPhoneTimeZoneSuggestion{"
+                    + "suggestion=" + suggestion
+                    + ", score=" + score
+                    + '}';
+        }
+    }
+}
diff --git a/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceHelperImpl.java b/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceHelperImpl.java
new file mode 100644
index 0000000..fc857a7
--- /dev/null
+++ b/src/java/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceHelperImpl.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2017 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.internal.telephony.nitz.service;
+
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.LocalLog;
+import android.util.Log;
+
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.PrintWriter;
+
+/**
+ * The real implementation of {@link TimeZoneDetectionService.Helper}.
+ */
+public final class TimeZoneDetectionServiceHelperImpl implements TimeZoneDetectionService.Helper {
+
+    private static final String LOG_TAG = TimeZoneDetectionService.LOG_TAG;
+    private static final boolean DBG = TimeZoneDetectionService.DBG;
+    private static final String TIMEZONE_PROPERTY = "persist.sys.timezone";
+
+    private final Context mContext;
+    private final ContentResolver mCr;
+    private final LocalLog mTimeZoneLog = new LocalLog(30);
+
+    private Listener mListener;
+
+    /** Creates a TimeServiceHelper */
+    public TimeZoneDetectionServiceHelperImpl(Context context) {
+        mContext = context;
+        mCr = context.getContentResolver();
+    }
+
+    @Override
+    public void setListener(Listener listener) {
+        if (listener == null) {
+            throw new NullPointerException("listener==null");
+        }
+        if (mListener != null) {
+            throw new IllegalStateException("listener already set");
+        }
+        this.mListener = listener;
+        mCr.registerContentObserver(
+                Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
+                new ContentObserver(new Handler()) {
+                    public void onChange(boolean selfChange) {
+                        listener.onTimeZoneDetectionChange(isTimeZoneDetectionEnabled());
+                    }
+                });
+    }
+
+    @Override
+    public boolean isTimeZoneDetectionEnabled() {
+        try {
+            return Settings.Global.getInt(mCr, Settings.Global.AUTO_TIME_ZONE) > 0;
+        } catch (Settings.SettingNotFoundException snfe) {
+            return true;
+        }
+    }
+
+    @Override
+    public boolean isTimeZoneSettingInitialized() {
+        // timezone.equals("GMT") will be true and only true if the time zone was
+        // set to a default value by the system server (when starting, system server
+        // sets the persist.sys.timezone to "GMT" if it's not set). "GMT" is not used by
+        // any code that sets it explicitly (in case where something sets GMT explicitly,
+        // "Etc/GMT" Olson ID would be used).
+
+        String timeZoneId = getTimeZoneSetting();
+        return timeZoneId != null && timeZoneId.length() > 0 && !timeZoneId.equals("GMT");
+    }
+
+    @Override
+    public void setDeviceTimeZoneFromSuggestion(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+        String currentZoneId = getTimeZoneSetting();
+        String newZoneId = timeZoneSuggestion.getZoneId();
+        if (newZoneId.equals(currentZoneId)) {
+            // No need to set the device time zone - the setting is already what we would be
+            // suggesting.
+            if (DBG) {
+                Log.d(LOG_TAG, "setDeviceTimeZoneAsNeeded: No need to change the time zone;"
+                        + " device is already set to the suggested zone."
+                        + " timeZoneSuggestion=" + timeZoneSuggestion);
+            }
+            return;
+        }
+
+        String msg = "Changing device time zone. currentZoneId=" + currentZoneId
+                + ", timeZoneSuggestion=" + timeZoneSuggestion;
+        if (DBG) {
+            Log.d(LOG_TAG, msg);
+        }
+        mTimeZoneLog.log(msg);
+
+        AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+        alarmManager.setTimeZone(newZoneId);
+        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
+        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+        intent.putExtra("time-zone", newZoneId);
+        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+    }
+
+    @Nullable
+    private String getTimeZoneSetting() {
+        return SystemProperties.get(TIMEZONE_PROPERTY);
+    }
+
+    @Override
+    public void dumpState(PrintWriter pw) {
+        pw.println(" TimeZoneDetectionServiceHelperImpl.getTimeZoneSetting()="
+                + getTimeZoneSetting());
+    }
+
+    @Override
+    public void dumpLogs(IndentingPrintWriter ipw) {
+        ipw.println("TimeZoneDetectionServiceHelperImpl:");
+
+        ipw.increaseIndent();
+        ipw.println("Time zone logs:");
+        ipw.increaseIndent();
+        mTimeZoneLog.dump(ipw);
+        ipw.decreaseIndent();
+
+        ipw.decreaseIndent();
+    }
+}
diff --git a/src/java/com/android/internal/telephony/sip/SipPhone.java b/src/java/com/android/internal/telephony/sip/SipPhone.java
index 82a3287..7fcaf96 100644
--- a/src/java/com/android/internal/telephony/sip/SipPhone.java
+++ b/src/java/com/android/internal/telephony/sip/SipPhone.java
@@ -29,9 +29,9 @@
 import android.os.Message;
 import android.telephony.DisconnectCause;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
 import android.telephony.ServiceState;
 import android.text.TextUtils;
-import android.telephony.Rlog;
 
 import com.android.internal.telephony.Call;
 import com.android.internal.telephony.CallStateException;
@@ -40,6 +40,8 @@
 import com.android.internal.telephony.PhoneConstants;
 import com.android.internal.telephony.PhoneNotifier;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.text.ParseException;
 import java.util.List;
 import java.util.regex.Pattern;
@@ -59,7 +61,9 @@
 
     // A call that is ringing or (call) waiting
     private SipCall mRingingCall = new SipCall();
+    @UnsupportedAppUsage
     private SipCall mForegroundCall = new SipCall();
+    @UnsupportedAppUsage
     private SipCall mBackgroundCall = new SipCall();
 
     private SipManager mSipManager;
@@ -431,6 +435,7 @@
         return false;
     }
 
+    @UnsupportedAppUsage
     private void log(String s) {
         Rlog.d(LOG_TAG, s);
     }
@@ -439,6 +444,7 @@
         Rlog.d(LOG_TAG, s);
     }
 
+    @UnsupportedAppUsage
     private void loge(String s) {
         Rlog.e(LOG_TAG, s);
     }
@@ -458,6 +464,7 @@
             setState(Call.State.IDLE);
         }
 
+        @UnsupportedAppUsage
         void switchWith(SipCall that) {
             if (SC_DBG) log("switchWith");
             synchronized (SipPhone.class) {
@@ -594,6 +601,7 @@
                     audioGroup.getMode()));
         }
 
+        @UnsupportedAppUsage
         void hold() throws CallStateException {
             if (SC_DBG) log("hold:");
             setState(State.HOLDING);
@@ -601,6 +609,7 @@
             setAudioGroupMode();
         }
 
+        @UnsupportedAppUsage
         void unhold() throws CallStateException {
             if (SC_DBG) log("unhold:");
             setState(State.ACTIVE);
diff --git a/src/java/com/android/internal/telephony/test/ModelInterpreter.java b/src/java/com/android/internal/telephony/test/ModelInterpreter.java
index 7930b56..2a046d9 100644
--- a/src/java/com/android/internal/telephony/test/ModelInterpreter.java
+++ b/src/java/com/android/internal/telephony/test/ModelInterpreter.java
@@ -20,6 +20,8 @@
 import android.os.Looper;
 import android.telephony.Rlog;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -121,6 +123,7 @@
 
 class InterpreterEx extends Exception
 {
+    @UnsupportedAppUsage
     public
     InterpreterEx (String result)
     {
diff --git a/src/java/com/android/internal/telephony/test/SimulatedCommands.java b/src/java/com/android/internal/telephony/test/SimulatedCommands.java
index 965d559..db581b2 100644
--- a/src/java/com/android/internal/telephony/test/SimulatedCommands.java
+++ b/src/java/com/android/internal/telephony/test/SimulatedCommands.java
@@ -69,6 +69,8 @@
 import com.android.internal.telephony.uicc.IccIoResult;
 import com.android.internal.telephony.uicc.IccSlotStatus;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -113,6 +115,7 @@
 
     //***** Instance Variables
 
+    @UnsupportedAppUsage
     SimulatedGsmCallState simulatedCallState;
     HandlerThread mHandlerThread;
     SimLockState mSimLockedState;
@@ -153,6 +156,7 @@
 
     int mNextCallFailCause = CallFailCause.NORMAL_CLEARING;
 
+    @UnsupportedAppUsage
     private boolean mDcSuccess = true;
     private SetupDataCallResult mSetupDataCallResult;
     private boolean mIsRadioPowerFailResponse = false;
@@ -801,6 +805,7 @@
      *  ar.userObject contains the original value of result.obj
      *  ar.result is null on success and failure
      */
+    @UnsupportedAppUsage
     @Override
     public void acceptCall (Message result) {
         boolean success;
@@ -1654,6 +1659,7 @@
 
     //***** Private Methods
 
+    @UnsupportedAppUsage
     private void unimplemented(Message result) {
         if (result != null) {
             AsyncResult.forMessage(result).exception
@@ -1667,6 +1673,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     private void resultSuccess(Message result, Object ret) {
         if (result != null) {
             AsyncResult.forMessage(result).result = ret;
@@ -1678,6 +1685,7 @@
         }
     }
 
+    @UnsupportedAppUsage
     private void resultFail(Message result, Object ret, Throwable tr) {
         if (result != null) {
             AsyncResult.forMessage(result, ret, tr);
diff --git a/src/java/com/android/internal/telephony/test/SimulatedCommandsVerifier.java b/src/java/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
index 13d8a9b..c550f2c 100644
--- a/src/java/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
+++ b/src/java/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
@@ -32,6 +32,8 @@
 import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
 import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo;
 
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
 public class SimulatedCommandsVerifier implements CommandsInterface {
     private static SimulatedCommandsVerifier sInstance;
 
@@ -39,6 +41,7 @@
 
     }
 
+    @UnsupportedAppUsage
     public static SimulatedCommandsVerifier getInstance() {
         if (sInstance == null) {
             sInstance = new SimulatedCommandsVerifier();
@@ -926,6 +929,7 @@
 
     }
 
+    @UnsupportedAppUsage
     @Override
     public void setCallForward(int action, int cfReason, int serviceClass, String number,
                                int timeSeconds, Message response) {
diff --git a/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java b/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java
index 75e84c4..72e2d93 100644
--- a/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java
+++ b/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java
@@ -16,16 +16,19 @@
 
 package com.android.internal.telephony.test;
 
+import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
-import android.os.Handler;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.Rlog;
+
 import com.android.internal.telephony.ATParseEx;
 import com.android.internal.telephony.DriverCall;
-import java.util.List;
-import java.util.ArrayList;
 
-import android.telephony.Rlog;
+import dalvik.annotation.compat.UnsupportedAppUsage;
+
+import java.util.ArrayList;
+import java.util.List;
 
 class CallInfo {
     enum State {
@@ -388,6 +391,7 @@
         return found;
     }
 
+    @UnsupportedAppUsage
     public boolean
     onChld(char c0, char c1) {
         boolean ret;
@@ -444,6 +448,7 @@
         return ret;
     }
 
+    @UnsupportedAppUsage
     public boolean
     releaseHeldOrUDUB() {
         boolean found = false;
@@ -474,6 +479,7 @@
     }
 
 
+    @UnsupportedAppUsage
     public boolean
     releaseActiveAcceptHeldOrWaiting() {
         boolean foundHeld = false;
@@ -529,6 +535,7 @@
         return true;
     }
 
+    @UnsupportedAppUsage
     public boolean
     switchActiveAndHeldOrWaiting() {
         boolean hasHeld = false;
@@ -562,6 +569,7 @@
     }
 
 
+    @UnsupportedAppUsage
     public boolean
     separateCall(int index) {
         try {
@@ -603,6 +611,7 @@
 
 
 
+    @UnsupportedAppUsage
     public boolean
     conference() {
         int countCalls = 0;
diff --git a/src/java/com/android/internal/telephony/uicc/IccCardApplicationStatus.java b/src/java/com/android/internal/telephony/uicc/IccCardApplicationStatus.java
index 76242c4..87bbc69 100644
--- a/src/java/com/android/internal/telephony/uicc/IccCardApplicationStatus.java
+++ b/src/java/com/android/internal/telephony/uicc/IccCardApplicationStatus.java
@@ -28,7 +28,14 @@
  * {@hide}
  */
 public class IccCardApplicationStatus {
+
+    @UnsupportedAppUsage
+    public IccCardApplicationStatus() {
+    }
+
     // TODO: Replace with constants from PhoneConstants.APPTYPE_xxx
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/uicc/IccCardApplicationStatus$AppType;")
     public enum AppType{
         @UnsupportedAppUsage
         APPTYPE_UNKNOWN,
@@ -44,6 +51,8 @@
         APPTYPE_ISIM
     }
 
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/uicc/IccCardApplicationStatus$AppState;")
     public enum AppState{
         @UnsupportedAppUsage
         APPSTATE_UNKNOWN,
@@ -80,6 +89,8 @@
         }
     }
 
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/uicc/IccCardApplicationStatus$PersoSubState;")
     public enum PersoSubState{
         @UnsupportedAppUsage
         PERSOSUBSTATE_UNKNOWN,
diff --git a/src/java/com/android/internal/telephony/uicc/IccRefreshResponse.java b/src/java/com/android/internal/telephony/uicc/IccRefreshResponse.java
index 7d0f845..ccb6f98 100644
--- a/src/java/com/android/internal/telephony/uicc/IccRefreshResponse.java
+++ b/src/java/com/android/internal/telephony/uicc/IccRefreshResponse.java
@@ -40,6 +40,10 @@
                                                   0x30, 0x30, 0x30 */
                                                /* Example: a0000000871002f310ffff89080000ff */
 
+    @UnsupportedAppUsage
+    public IccRefreshResponse() {
+    }
+
     @Override
     public String toString() {
         return "{" + refreshResult + ", " + aid +", " + efId + "}";
diff --git a/src/java/com/android/internal/telephony/uicc/SIMRecords.java b/src/java/com/android/internal/telephony/uicc/SIMRecords.java
index 6917563..663ebd6 100644
--- a/src/java/com/android/internal/telephony/uicc/SIMRecords.java
+++ b/src/java/com/android/internal/telephony/uicc/SIMRecords.java
@@ -1690,6 +1690,8 @@
     /**
      * States of Get SPN Finite State Machine which only used by getSpnFsm()
      */
+    @UnsupportedAppUsage(implicitMember =
+            "values()[Lcom/android/internal/telephony/uicc/SIMRecords$GetSpnFsmState;")
     private enum GetSpnFsmState {
         IDLE,               // No initialized
         @UnsupportedAppUsage
diff --git a/src/java/com/android/internal/telephony/uicc/UiccController.java b/src/java/com/android/internal/telephony/uicc/UiccController.java
index 0cb8c1c..6112dda 100644
--- a/src/java/com/android/internal/telephony/uicc/UiccController.java
+++ b/src/java/com/android/internal/telephony/uicc/UiccController.java
@@ -830,7 +830,17 @@
         boolean isDefaultEuiccCardIdSet = false;
         boolean anyEuiccIsActive = false;
         mHasActiveBuiltInEuicc = false;
-        for (int i = 0; i < status.size(); i++) {
+
+        int numSlots = status.size();
+        if (mUiccSlots.length < numSlots) {
+            String logStr = "The number of the physical slots reported " + numSlots
+                    + " is greater than the expectation " + mUiccSlots.length + ".";
+            Rlog.e(LOG_TAG, logStr);
+            sLocalLog.log(logStr);
+            numSlots = mUiccSlots.length;
+        }
+
+        for (int i = 0; i < numSlots; i++) {
             IccSlotStatus iss = status.get(i);
             boolean isActive = (iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_ACTIVE);
             if (isActive) {
@@ -895,7 +905,7 @@
             // Note that on HAL<1.2, it's possible that a built-in eUICC exists, but does not
             // correspond to any slot in mUiccSlots. This logic is still safe in that case because
             // SlotStatus is only for HAL >= 1.2
-            for (int i = 0; i < status.size(); i++) {
+            for (int i = 0; i < numSlots; i++) {
                 if (mUiccSlots[i].isEuicc()) {
                     String eid = status.get(i).eid;
                     if (!TextUtils.isEmpty(eid)) {
diff --git a/src/java/com/android/internal/telephony/util/TelephonyUtils.java b/src/java/com/android/internal/telephony/util/TelephonyUtils.java
new file mode 100644
index 0000000..1048e5c
--- /dev/null
+++ b/src/java/com/android/internal/telephony/util/TelephonyUtils.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 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.internal.telephony.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+import android.content.pm.ComponentInfo;
+import android.content.pm.ResolveInfo;
+
+/**
+ * This class provides various util functions
+ */
+public final class TelephonyUtils {
+    /** {@hide} */
+    public static String emptyIfNull(@Nullable String str) {
+        return str == null ? "" : str;
+    }
+
+    public static boolean IS_DEBUGGABLE =
+            SystemProperties.getInt("ro.debuggable", 0) == 1;
+
+    public static ComponentInfo getComponentInfo(@NonNull ResolveInfo resolveInfo) {
+        if (resolveInfo.activityInfo != null) return resolveInfo.activityInfo;
+        if (resolveInfo.serviceInfo != null) return resolveInfo.serviceInfo;
+        if (resolveInfo.providerInfo != null) return resolveInfo.providerInfo;
+        throw new IllegalStateException("Missing ComponentInfo!");
+    }
+  }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupport.java b/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupport.java
index d023c79..02a0cc9 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupport.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupport.java
@@ -34,6 +34,7 @@
     // Values used to when initializing device state but where the value isn't important.
     public static final long ARBITRARY_SYSTEM_CLOCK_TIME = createUtcTime(1977, 1, 1, 12, 0, 0);
     public static final long ARBITRARY_REALTIME_MILLIS = 123456789L;
+    // This zone isn't used in any of the scenarios below.
     public static final String ARBITRARY_TIME_ZONE_ID = "Europe/Paris";
     public static final String ARBITRARY_DEBUG_INFO = "Test debug info";
 
@@ -44,6 +45,7 @@
             .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
             .setCountryIso("gb")
             .buildFrozen();
+
     public static final String UNITED_KINGDOM_COUNTRY_DEFAULT_ZONE_ID = "Europe/London";
 
     // The US is a country that has multiple zones, but there is only one matching time zone at the
@@ -62,6 +64,17 @@
             .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
             .setCountryIso("us")
             .buildFrozen();
+
+    // A non-unique US scenario: the offset information is ambiguous between America/Phoenix and
+    // America/Denver during winter.
+    public static final Scenario NON_UNIQUE_US_ZONE_SCENARIO = new Scenario.Builder()
+            .setTimeZone("America/Denver")
+            .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
+            .setCountryIso("us")
+            .buildFrozen();
+    public static final String[] NON_UNIQUE_US_ZONE_SCENARIO_ZONES =
+            { "America/Denver", "America/Phoenix" };
+
     public static final String US_COUNTRY_DEFAULT_ZONE_ID = "America/New_York";
 
     // New Zealand is a country with multiple zones, but the default zone has the "boost" modifier
@@ -76,6 +89,7 @@
             .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
             .setCountryIso("nz")
             .buildFrozen();
+
     public static final String NEW_ZEALAND_COUNTRY_DEFAULT_ZONE_ID = "Pacific/Auckland";
 
     // A country with a single zone: the zone can be guessed from the country alone. CZ never uses
@@ -85,6 +99,7 @@
             .setActualTimeUtc(2018, 1, 1, 12, 0, 0)
             .setCountryIso("cz")
             .buildFrozen();
+
     public static final String CZECHIA_COUNTRY_DEFAULT_ZONE_ID = "Europe/Prague";
 
     /**
diff --git a/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupportTest.java b/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupportTest.java
index 6f5eed0..4df0adf 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupportTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/NitzStateMachineTestSupportTest.java
@@ -23,6 +23,8 @@
 import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_COUNTRY_DEFAULT_ZONE_ID;
 import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_DEFAULT_SCENARIO;
 import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_OTHER_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NON_UNIQUE_US_ZONE_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NON_UNIQUE_US_ZONE_SCENARIO_ZONES;
 import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO1;
 import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO2;
 import static com.android.internal.telephony.NitzStateMachineTestSupport.UNITED_KINGDOM_COUNTRY_DEFAULT_ZONE_ID;
@@ -33,6 +35,8 @@
 import static com.android.internal.telephony.TimeZoneLookupHelper.CountryResult.QUALITY_SINGLE_ZONE;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import com.android.internal.telephony.TimeZoneLookupHelper.CountryResult;
 import com.android.internal.telephony.TimeZoneLookupHelper.OffsetResult;
@@ -40,6 +44,9 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.List;
+
 public class NitzStateMachineTestSupportTest {
 
     private TimeZoneLookupHelper mTimeZoneLookupHelper;
@@ -87,6 +94,32 @@
     }
 
     @Test
+    public void test_nonUniqueUs_assumptions() {
+        // Check we'll get the expected behavior from TimeZoneLookupHelper.
+
+        // quality == QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS, therefore the country's default zone
+        // shouldn't be considered a good match.
+        CountryResult expectedCountryLookupResult = new CountryResult(
+                US_COUNTRY_DEFAULT_ZONE_ID, QUALITY_MULTIPLE_ZONES_DIFFERENT_OFFSETS,
+                ARBITRARY_DEBUG_INFO);
+        CountryResult actualCountryLookupResult =
+                mTimeZoneLookupHelper.lookupByCountry(
+                        NON_UNIQUE_US_ZONE_SCENARIO.getNetworkCountryIsoCode(),
+                        ARBITRARY_SYSTEM_CLOCK_TIME);
+        assertEquals(expectedCountryLookupResult, actualCountryLookupResult);
+
+        // By definition, there are multiple matching zones for the NON_UNIQUE_US_ZONE_SCENARIO.
+        {
+            OffsetResult actualLookupResult = mTimeZoneLookupHelper.lookupByNitzCountry(
+                    NON_UNIQUE_US_ZONE_SCENARIO.createNitzData(),
+                    NON_UNIQUE_US_ZONE_SCENARIO.getNetworkCountryIsoCode());
+            List<String> possibleZones = Arrays.asList(NON_UNIQUE_US_ZONE_SCENARIO_ZONES);
+            assertTrue(possibleZones.contains(actualLookupResult.getTimeZone().getID()));
+            assertFalse(actualLookupResult.getIsOnlyMatch());
+        }
+    }
+
+    @Test
     public void test_unitedKingdom_assumptions() {
         assertEquals(UNITED_KINGDOM_SCENARIO.getTimeZone().getID(),
                 UNITED_KINGDOM_COUNTRY_DEFAULT_ZONE_ID);
diff --git a/tests/telephonytests/src/com/android/internal/telephony/PhoneSubInfoControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/PhoneSubInfoControllerTest.java
index ff57227..a69a9a6 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/PhoneSubInfoControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/PhoneSubInfoControllerTest.java
@@ -53,6 +53,7 @@
         doReturn(0).when(mSubscriptionController).getPhoneId(eq(0));
         doReturn(1).when(mSubscriptionController).getPhoneId(eq(1));
         doReturn(2).when(mTelephonyManager).getPhoneCount();
+        doReturn(2).when(mTelephonyManager).getActiveModemCount();
         doReturn(true).when(mSubscriptionController).isActiveSubId(0, TAG);
         doReturn(true).when(mSubscriptionController).isActiveSubId(1, TAG);
         doReturn(new int[]{0, 1}).when(mSubscriptionManager)
diff --git a/tests/telephonytests/src/com/android/internal/telephony/PhoneSwitcherTest.java b/tests/telephonytests/src/com/android/internal/telephony/PhoneSwitcherTest.java
index 62da291..fe0e74f 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/PhoneSwitcherTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/PhoneSwitcherTest.java
@@ -1040,6 +1040,7 @@
         }
 
         doReturn(numPhones).when(mTelephonyManager).getPhoneCount();
+        doReturn(numPhones).when(mTelephonyManager).getActiveModemCount();
         if (numPhones == 1) {
             mCommandsInterfaces = new CommandsInterface[] {mCommandsInterface0};
             mPhones = new Phone[] {mPhone};
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
index 7d3f6e8..ce3281d 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionControllerTest.java
@@ -964,6 +964,7 @@
     public void testGetEnabledSubscriptionIdDualSIM() {
         doReturn(SINGLE_SIM).when(mTelephonyManager).getSimCount();
         doReturn(SINGLE_SIM).when(mTelephonyManager).getPhoneCount();
+        doReturn(SINGLE_SIM).when(mTelephonyManager).getActiveModemCount();
         // A dual SIM device may have logical slot 0 mapped to physical slot 0
         // (i.e. logical slot 1 mapped to physical slot 1)
         UiccSlotInfo slot0 = getFakeUiccSlotInfo(true, 0);
@@ -972,6 +973,7 @@
         UiccSlot [] uiccSlots = {mUiccSlot, mUiccSlot};
 
         doReturn(2).when(mTelephonyManager).getPhoneCount();
+        doReturn(2).when(mTelephonyManager).getActiveModemCount();
         doReturn(uiccSlotInfos).when(mTelephonyManager).getUiccSlotsInfo();
         doReturn(uiccSlots).when(mUiccController).getUiccSlots();
         assertEquals(2, UiccController.getInstance().getUiccSlots().length);
diff --git a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionInfoUpdaterTest.java b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionInfoUpdaterTest.java
index c1cbe8b..6c34903 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/SubscriptionInfoUpdaterTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/SubscriptionInfoUpdaterTest.java
@@ -120,7 +120,7 @@
 
         replaceInstance(SubscriptionInfoUpdater.class, "sIccId", null, new String[1]);
         replaceInstance(SubscriptionInfoUpdater.class, "sContext", null, null);
-        replaceInstance(SubscriptionInfoUpdater.class, "PROJECT_SIM_NUM", null, 1);
+        replaceInstance(SubscriptionInfoUpdater.class, "SUPPORTED_MODEM_COUNT", null, 1);
         replaceInstance(SubscriptionInfoUpdater.class, "sSimCardState", null, new int[1]);
         replaceInstance(SubscriptionInfoUpdater.class, "sSimApplicationState", null, new int[1]);
         replaceInstance(SubscriptionInfoUpdater.class, "sIsSubInfoInitialized", null, false);
@@ -132,6 +132,7 @@
         doReturn(mUiccSlot).when(mUiccController).getUiccSlotForPhone(anyInt());
         doReturn(1).when(mTelephonyManager).getSimCount();
         doReturn(1).when(mTelephonyManager).getPhoneCount();
+        doReturn(1).when(mTelephonyManager).getActiveModemCount();
 
         when(mContentProvider.update(any(), any(), any(), isNull())).thenAnswer(
                 new Answer<Integer>() {
@@ -389,7 +390,7 @@
         replaceInstance(PhoneFactory.class, "sPhones", null, new Phone[]{mPhone, mPhone});
         replaceInstance(SubscriptionInfoUpdater.class, "sIccId", null,
                 new String[]{null, null});
-        replaceInstance(SubscriptionInfoUpdater.class, "PROJECT_SIM_NUM", null, 2);
+        replaceInstance(SubscriptionInfoUpdater.class, "SUPPORTED_MODEM_COUNT", null, 2);
         replaceInstance(SubscriptionInfoUpdater.class, "sSimCardState", null,
                 new int[]{0, 0});
         replaceInstance(SubscriptionInfoUpdater.class, "sSimApplicationState", null,
@@ -400,6 +401,7 @@
         doReturn(FAKE_SUB_ID_1).when(mSubscriptionController).getPhoneId(eq(FAKE_SUB_ID_1));
         doReturn(FAKE_SUB_ID_2).when(mSubscriptionController).getPhoneId(eq(FAKE_SUB_ID_2));
         doReturn(2).when(mTelephonyManager).getPhoneCount();
+        doReturn(2).when(mTelephonyManager).getActiveModemCount();
         doReturn(FAKE_MCC_MNC_1).when(mTelephonyManager).getSimOperatorNumeric(eq(FAKE_SUB_ID_1));
         doReturn(FAKE_MCC_MNC_2).when(mTelephonyManager).getSimOperatorNumeric(eq(FAKE_SUB_ID_2));
         verify(mSubscriptionController, times(0)).clearSubInfo();
diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyPermissionsTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyPermissionsTest.java
index f2d4a17..08defe3 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/TelephonyPermissionsTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyPermissionsTest.java
@@ -32,8 +32,11 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ServiceManager;
 import android.provider.DeviceConfig;
 import android.provider.Settings;
 import android.telephony.SubscriptionManager;
@@ -50,14 +53,15 @@
 import org.mockito.MockitoAnnotations;
 
 import java.lang.reflect.Field;
+import java.util.Map;
 
 @SmallTest
 public class TelephonyPermissionsTest {
 
     private static final int SUB_ID = 55555;
     private static final int SUB_ID_2 = 22222;
-    private static final int PID = 12345;
-    private static final int UID = 54321;
+    private static final int PID = Binder.getCallingPid();
+    private static final int UID = Binder.getCallingUid();
     private static final String PACKAGE = "com.example";
     private static final String MSG = "message";
 
@@ -70,6 +74,8 @@
     @Mock
     private ITelephony mMockTelephony;
     @Mock
+    private IBinder mMockTelephonyBinder;
+    @Mock
     private PackageManager mMockPackageManager;
     @Mock
     private ApplicationInfo mMockApplicationInfo;
@@ -101,10 +107,13 @@
                 AppOpsManager.MODE_ERRORED);
         when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID), eq(UID)))
                 .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
+        when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID_2), eq(UID)))
+                .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS);
         when(mMockContext.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                 PID, UID)).thenReturn(PackageManager.PERMISSION_DENIED);
         when(mMockDevicePolicyManager.checkDeviceIdentifierAccess(eq(PACKAGE), eq(PID),
                 eq(UID))).thenReturn(false);
+        setTelephonyMockAsService();
     }
 
     @Test
@@ -243,8 +252,8 @@
     public void testCheckReadDeviceIdentifiers_noPermissions() throws Exception {
         setupMocksForDeviceIdentifiersErrorPath();
         try {
-            TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                    SUB_ID, PID, UID, PACKAGE, MSG);
+            TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                    SUB_ID, PACKAGE, MSG);
             fail("Should have thrown SecurityException");
         } catch (SecurityException e) {
             // expected
@@ -256,8 +265,8 @@
         when(mMockContext.checkPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                 PID, UID)).thenReturn(PackageManager.PERMISSION_GRANTED);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -265,8 +274,8 @@
         when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID), eq(UID)))
                 .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -274,8 +283,8 @@
         when(mMockAppOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, UID,
                 PACKAGE)).thenReturn(AppOpsManager.MODE_ALLOWED);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -283,8 +292,8 @@
         when(mMockDevicePolicyManager.checkDeviceIdentifierAccess(eq(PACKAGE), eq(PID),
                 eq(UID))).thenReturn(true);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -296,8 +305,8 @@
                 UID)).thenReturn(PackageManager.PERMISSION_GRANTED);
         setupMocksForDeviceIdentifiersErrorPath();
         try {
-            TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                    SUB_ID, PID, UID, PACKAGE, MSG);
+            TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                    SUB_ID, PACKAGE, MSG);
             fail("Should have thrown SecurityException");
         } catch (SecurityException e) {
             // expected
@@ -314,8 +323,8 @@
         setupMocksForDeviceIdentifiersErrorPath();
         mMockApplicationInfo.targetSdkVersion = Build.VERSION_CODES.P;
         assertFalse(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -326,8 +335,8 @@
         when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID_2), eq(UID))).thenReturn(
                 TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -340,8 +349,8 @@
         when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID_2), eq(UID)))
                 .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID_2, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -354,8 +363,8 @@
         when(mMockAppOps.noteOpNoThrow(AppOpsManager.OPSTR_READ_DEVICE_IDENTIFIERS, UID,
                 PACKAGE)).thenReturn(AppOpsManager.MODE_ALLOWED);
         assertTrue(
-                TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                        SUB_ID, PID, UID, PACKAGE, MSG));
+                TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
     }
 
     @Test
@@ -365,14 +374,79 @@
         // case.
         setupMocksForDeviceIdentifiersErrorPath();
         try {
-            TelephonyPermissions.checkReadDeviceIdentifiers(mMockContext, () -> mMockTelephony,
-                    SUB_ID, PID, UID, null, MSG);
+            TelephonyPermissions.checkCallingOrSelfReadDeviceIdentifiers(mMockContext,
+                    SUB_ID, null, MSG);
             fail("Should have thrown SecurityException");
         } catch (SecurityException e) {
             // expected
         }
     }
 
+    @Test
+    public void testCheckCallingOrSelfReadSubscriberIdentifiers_noPermissions() throws Exception {
+        setupMocksForDeviceIdentifiersErrorPath();
+        setTelephonyMockAsService();
+        when(mMockContext.checkPermission(
+                eq(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE),
+                anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
+        when(mMockAppOps.noteOpNoThrow(anyString(), anyInt(), eq(PACKAGE))).thenReturn(
+                AppOpsManager.MODE_ERRORED);
+        when(mMockDevicePolicyManager.checkDeviceIdentifierAccess(eq(PACKAGE), anyInt(),
+                anyInt())).thenReturn(false);
+        try {
+            TelephonyPermissions.checkCallingOrSelfReadSubscriberIdentifiers(mMockContext,
+                    SUB_ID, PACKAGE, MSG);
+            fail("Should have thrown SecurityException");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testCheckCallingOrSelfReadSubscriberIdentifiers_carrierPrivileges()
+            throws Exception {
+        setTelephonyMockAsService();
+        when(mMockContext.checkPermission(
+                eq(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE),
+                anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
+        when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID), anyInt()))
+                .thenReturn(TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
+        assertTrue(
+                TelephonyPermissions.checkCallingOrSelfReadSubscriberIdentifiers(mMockContext,
+                        SUB_ID, PACKAGE, MSG));
+    }
+
+    @Test
+    public void testCheckCallingOrSelfReadSubscriberIdentifiers_carrierPrivilegesOnOtherSub()
+            throws Exception {
+        setupMocksForDeviceIdentifiersErrorPath();
+        setTelephonyMockAsService();
+        when(mMockContext.checkPermission(
+                eq(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE),
+                anyInt(), anyInt())).thenReturn(PackageManager.PERMISSION_DENIED);
+        when(mMockSubscriptionManager.getActiveSubscriptionIdList(anyBoolean())).thenReturn(
+                new int[]{SUB_ID, SUB_ID_2});
+        when(mMockTelephony.getCarrierPrivilegeStatusForUid(eq(SUB_ID_2), anyInt())).thenReturn(
+                TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS);
+        // Carrier privilege on the other active sub shouldn't allow access to this sub.
+        try {
+            TelephonyPermissions.checkCallingOrSelfReadSubscriberIdentifiers(mMockContext,
+                    SUB_ID, PACKAGE, MSG);
+            fail("Should have thrown SecurityException");
+        } catch (SecurityException e) {
+            // expected
+        }
+    }
+
+    // Put mMockTelephony into service cache so that TELEPHONY_SUPPLIER will get it.
+    private void setTelephonyMockAsService() throws Exception {
+        when(mMockTelephonyBinder.queryLocalInterface(anyString())).thenReturn(mMockTelephony);
+        Field field = ServiceManager.class.getDeclaredField("sCache");
+        field.setAccessible(true);
+        ((Map<String, IBinder>) field.get(null)).put(Context.TELEPHONY_SERVICE,
+                mMockTelephonyBinder);
+    }
+
     public static class FakeSettingsConfigProvider extends FakeSettingsProvider {
         private static final String PROPERTY_DEVICE_IDENTIFIER_ACCESS_RESTRICTIONS_DISABLED =
                 DeviceConfig.NAMESPACE_PRIVACY + "/"
diff --git a/tests/telephonytests/src/com/android/internal/telephony/nitz/NewNitzStateMachineImplTest.java b/tests/telephonytests/src/com/android/internal/telephony/nitz/NewNitzStateMachineImplTest.java
new file mode 100644
index 0000000..d422687
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/nitz/NewNitzStateMachineImplTest.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import static com.android.internal.telephony.NitzStateMachineTestSupport.ARBITRARY_SYSTEM_CLOCK_TIME;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO1;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNITED_KINGDOM_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.createTimeSuggestionFromNitzSignal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.timedetector.PhoneTimeSuggestion;
+import android.util.TimestampedValue;
+
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachineTestSupport.FakeDeviceState;
+import com.android.internal.telephony.NitzStateMachineTestSupport.Scenario;
+import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.TimeZoneLookupHelper;
+import com.android.internal.telephony.nitz.NewNitzStateMachineImpl.NitzSignalInputFilterPredicate;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+import com.android.internal.util.IndentingPrintWriter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.PrintWriter;
+import java.util.LinkedList;
+import java.util.concurrent.TimeUnit;
+
+public class NewNitzStateMachineImplTest extends TelephonyTest {
+
+    private static final int PHONE_ID = 99999;
+    private static final PhoneTimeZoneSuggestion EMPTY_TIME_ZONE_SUGGESTION =
+            new PhoneTimeZoneSuggestion(PHONE_ID);
+
+    private FakeNewTimeServiceHelper mFakeNewTimeServiceHelper;
+    private FakeDeviceState mFakeDeviceState;
+    private TimeZoneSuggesterImpl mRealTimeZoneSuggester;
+
+    private NewNitzStateMachineImpl mNitzStateMachineImpl;
+
+
+    @Before
+    public void setUp() throws Exception {
+        TelephonyTest.logd("NewNitzStateMachineImplTest +Setup!");
+        super.setUp("NewNitzStateMachineImplTest");
+
+        // In tests we use a fake impls for NewTimeServiceHelper and DeviceState.
+        mFakeDeviceState = new FakeDeviceState();
+        mFakeNewTimeServiceHelper = new FakeNewTimeServiceHelper(mFakeDeviceState);
+
+        // In tests we disable NITZ signal input filtering. The real NITZ signal filter is tested
+        // independently. This makes constructing test data simpler: we can be sure the signals
+        // won't be filtered for reasons like rate-limiting.
+        NitzSignalInputFilterPredicate mFakeNitzSignalInputFilter = (oldSignal, newSignal) -> true;
+
+        // In tests a real TimeZoneSuggesterImpl is used with the real TimeZoneLookupHelper and real
+        // country time zone data. A fake device state is used (which allows tests to fake the
+        // system clock / user settings). The tests can perform the expected lookups and confirm the
+        // state machine takes the correct action. Picking real examples from the past is easier
+        // than inventing countries / scenarios and configuring fakes.
+        TimeZoneLookupHelper timeZoneLookupHelper = new TimeZoneLookupHelper();
+        mRealTimeZoneSuggester = new TimeZoneSuggesterImpl(mFakeDeviceState, timeZoneLookupHelper);
+
+        mNitzStateMachineImpl = new NewNitzStateMachineImpl(
+                PHONE_ID, mFakeNitzSignalInputFilter, mRealTimeZoneSuggester,
+                mFakeNewTimeServiceHelper, mFakeDeviceState);
+
+        TelephonyTest.logd("NewNitzStateMachineImplTest -Setup!");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void test_countryThenNitz() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        String networkCountryIsoCode = scenario.getNetworkCountryIsoCode();
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        // Capture expected results from the real suggester and confirm we can tell the difference
+        // between them.
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion1 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, networkCountryIsoCode, null /* nitzSignal */);
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion2 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, networkCountryIsoCode, nitzSignal);
+        assertNotNull(expectedTimeZoneSuggestion2);
+        assertNotEquals(expectedTimeZoneSuggestion1, expectedTimeZoneSuggestion2);
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate country being known.
+        script.countryReceived(networkCountryIsoCode);
+
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion1);
+
+        // Check NitzStateMachine exposed state.
+        assertNull(mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate NITZ being received and verify the behavior.
+        script.nitzReceived(nitzSignal);
+
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, expectedTimeZoneSuggestion2);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_nitzThenCountry() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        String networkCountryIsoCode = scenario.getNetworkCountryIsoCode();
+
+        // Capture test expectations from the real suggester and confirm we can tell the difference
+        // between them.
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion1 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, null /* countryIsoCode */, nitzSignal);
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion2 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, networkCountryIsoCode, nitzSignal);
+        assertNotEquals(expectedTimeZoneSuggestion1, expectedTimeZoneSuggestion2);
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate receiving the NITZ signal.
+        script.nitzReceived(nitzSignal);
+
+        // Verify the state machine did the right thing.
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, expectedTimeZoneSuggestion1);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate country being known and verify the behavior.
+        script.countryReceived(networkCountryIsoCode)
+                .verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion2);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_emptyCountryString_countryReceivedFirst() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate an empty country being set.
+        script.countryReceived("");
+
+        // Nothing should be set. The country is not valid.
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(EMPTY_TIME_ZONE_SUGGESTION);
+
+        // Check NitzStateMachine exposed state.
+        assertNull(mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate receiving the NITZ signal.
+        script.nitzReceived(nitzSignal);
+
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        // Capture output from the real suggester and confirm it meets the test's needs /
+        // expectations.
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, "" /* countryIsoCode */, nitzSignal);
+        assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                expectedTimeZoneSuggestion.getMatchType());
+        assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                expectedTimeZoneSuggestion.getQuality());
+
+        // Verify the state machine did the right thing.
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, expectedTimeZoneSuggestion);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_emptyCountryStringUsTime_nitzReceivedFirst() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate receiving the NITZ signal.
+        script.nitzReceived(nitzSignal);
+
+        // Verify the state machine did the right thing.
+        // No time zone should be set. A NITZ signal by itself is not enough.
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, EMPTY_TIME_ZONE_SUGGESTION);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate an empty country being set.
+        script.countryReceived("");
+
+        // Capture output from the real suggester and confirm it meets the test's needs /
+        // expectations.
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, "" /* countryIsoCode */, nitzSignal);
+        assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                expectedTimeZoneSuggestion.getMatchType());
+        assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                expectedTimeZoneSuggestion.getQuality());
+
+        // Verify the state machine did the right thing.
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion);
+
+        // Check NitzStateMachine exposed state.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_airplaneModeClearsState() throws Exception {
+        Scenario scenario = UNITED_KINGDOM_SCENARIO.mutableCopy();
+        int timeStepMillis = (int) TimeUnit.HOURS.toMillis(3);
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Pre-flight: Simulate a device receiving signals that allow it to detect time and time
+        // zone.
+        TimestampedValue<NitzData> preFlightNitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        PhoneTimeSuggestion expectedPreFlightTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, preFlightNitzSignal);
+        String preFlightCountryIsoCode = scenario.getNetworkCountryIsoCode();
+
+        // Simulate receiving the NITZ signal and country.
+        script.nitzReceived(preFlightNitzSignal)
+                .countryReceived(preFlightCountryIsoCode);
+
+        // Verify the state machine did the right thing.
+        PhoneTimeZoneSuggestion expectedPreFlightTimeZoneSuggestion =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, preFlightCountryIsoCode, preFlightNitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedPreFlightTimeSuggestion, expectedPreFlightTimeZoneSuggestion);
+
+        // Check state that NitzStateMachine must expose.
+        assertEquals(preFlightNitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+
+        // Boarded flight: Airplane mode turned on / time zone detection still enabled.
+        // The NitzStateMachine must lose all state and stop having an opinion about time zone.
+
+        // Simulate the passage of time and update the device realtime clock.
+        scenario.incrementTime(timeStepMillis);
+        script.incrementTime(timeStepMillis);
+
+        // Simulate airplane mode being turned on.
+        script.toggleAirplaneMode(true);
+
+        // Verify the state machine did the right thing.
+        // Check the time and time zone suggestion was withdrawn.
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(EMPTY_TIME_ZONE_SUGGESTION);
+
+        // Check state that NitzStateMachine must expose.
+        assertNull(mNitzStateMachineImpl.getCachedNitzData());
+
+        // During flight: Airplane mode turned off / time zone detection still enabled.
+        // The NitzStateMachine still must not have an opinion about time zone / hold any state.
+
+        // Simulate the passage of time and update the device realtime clock.
+        scenario.incrementTime(timeStepMillis);
+        script.incrementTime(timeStepMillis);
+
+        // Simulate airplane mode being turned off.
+        script.toggleAirplaneMode(false);
+
+        // Verify the time zone suggestion was withdrawn.
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(EMPTY_TIME_ZONE_SUGGESTION);
+
+        // Check the state that NitzStateMachine must expose.
+        assertNull(mNitzStateMachineImpl.getCachedNitzData());
+
+        // Post flight: Device has moved and receives new signals.
+
+        // Simulate the passage of time and update the device realtime clock.
+        scenario.incrementTime(timeStepMillis);
+        script.incrementTime(timeStepMillis);
+
+        // Simulate the movement to the destination.
+        scenario.changeCountry(UNIQUE_US_ZONE_SCENARIO1.getTimeZoneId(),
+                UNIQUE_US_ZONE_SCENARIO1.getNetworkCountryIsoCode());
+
+        // Simulate the device receiving NITZ signal and country again after the flight. Now the
+        // NitzStateMachine should be opinionated again.
+        TimestampedValue<NitzData> postFlightNitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        String postFlightCountryCode = scenario.getNetworkCountryIsoCode();
+        script.countryReceived(postFlightCountryCode)
+                .nitzReceived(postFlightNitzSignal);
+
+        // Verify the state machine did the right thing.
+        PhoneTimeSuggestion expectedPostFlightTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, postFlightNitzSignal);
+        PhoneTimeZoneSuggestion expectedPostFlightTimeZoneSuggestion =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, postFlightCountryCode, postFlightNitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedPostFlightTimeSuggestion, expectedPostFlightTimeZoneSuggestion);
+
+        // Check state that NitzStateMachine must expose.
+        assertEquals(postFlightNitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_countryUnavailableClearsTimeZoneSuggestion() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate receiving the country and verify the state machine does the right thing.
+        script.countryReceived(scenario.getNetworkCountryIsoCode());
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion1 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion1);
+
+        // Simulate receiving an NITZ signal and verify the state machine does the right thing.
+        script.nitzReceived(nitzSignal);
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion2 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, expectedTimeZoneSuggestion2);
+
+        // Check state that NitzStateMachine must expose.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate the country becoming unavailable and verify the state machine does the right
+        // thing.
+        script.countryUnavailable();
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion3 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, null /* countryIsoCode */, nitzSignal);
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion3);
+
+        // Check state that NitzStateMachine must expose.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    @Test
+    public void test_networkAvailableClearsCacheNitzAndTimeZoneSuggestion() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        Script script = new Script()
+                .initializeSystemClock(ARBITRARY_SYSTEM_CLOCK_TIME)
+                .networkAvailable();
+
+        // Simulate receiving the country and verify the state machine does the right thing.
+        script.countryReceived(scenario.getNetworkCountryIsoCode());
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion1 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion1);
+
+        // Simulate receiving an NITZ signal and verify the state machine does the right thing.
+        script.nitzReceived(nitzSignal);
+        PhoneTimeSuggestion expectedTimeSuggestion =
+                createTimeSuggestionFromNitzSignal(PHONE_ID, nitzSignal);
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion2 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+        script.verifyTimeAndTimeZoneSuggestedAndReset(
+                expectedTimeSuggestion, expectedTimeZoneSuggestion2);
+
+        // Check state that NitzStateMachine must expose.
+        assertEquals(nitzSignal.getValue(), mNitzStateMachineImpl.getCachedNitzData());
+
+        // Simulate network becoming available and verify the state machine does the right thing.
+        script.networkAvailable();
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion3 =
+                mRealTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+        script.verifyOnlyTimeZoneWasSuggestedAndReset(expectedTimeZoneSuggestion3);
+
+        // Check state that NitzStateMachine must expose.
+        assertNull(mNitzStateMachineImpl.getCachedNitzData());
+    }
+
+    /**
+     * A "fluent" helper class allowing reuse of logic for test state initialization, simulation of
+     * events, and verification of device state changes with self-describing method names.
+     */
+    private class Script {
+
+        Script() {
+            // Set initial fake device state.
+            mFakeDeviceState.ignoreNitz = false;
+            mFakeDeviceState.nitzUpdateDiffMillis = 2000;
+            mFakeDeviceState.nitzUpdateSpacingMillis = 1000 * 60 * 10;
+
+            mFakeDeviceState.networkCountryIsoForPhone = "";
+        }
+
+        // Initialization methods for setting simulated device state, usually before simulation.
+
+        Script initializeSystemClock(long timeMillis) {
+            mFakeDeviceState.currentTimeMillis = timeMillis;
+            return this;
+        }
+
+        // Simulation methods that are used by tests to pretend that something happens.
+
+        Script incrementTime(int timeIncrementMillis) {
+            mFakeDeviceState.simulateTimeIncrement(timeIncrementMillis);
+            return this;
+        }
+
+        Script networkAvailable() {
+            mNitzStateMachineImpl.handleNetworkAvailable();
+            return this;
+        }
+
+        Script countryUnavailable() {
+            mNitzStateMachineImpl.handleNetworkCountryCodeUnavailable();
+            return this;
+        }
+
+        Script countryReceived(String countryIsoCode) {
+            mFakeDeviceState.networkCountryIsoForPhone = countryIsoCode;
+            mNitzStateMachineImpl.handleNetworkCountryCodeSet(true);
+            return this;
+        }
+
+        Script nitzReceived(TimestampedValue<NitzData> nitzSignal) {
+            mNitzStateMachineImpl.handleNitzReceived(nitzSignal);
+            return this;
+        }
+
+        Script toggleAirplaneMode(boolean on) {
+            mNitzStateMachineImpl.handleAirplaneModeChanged(on);
+            return this;
+        }
+
+        // Verification methods.
+
+        Script verifyOnlyTimeZoneWasSuggestedAndReset(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            justVerifyTimeZoneWasSuggested(timeZoneSuggestion);
+            justVerifyTimeWasNotSuggested();
+            commitStateChanges();
+            return this;
+        }
+
+        Script verifyTimeAndTimeZoneSuggestedAndReset(
+                PhoneTimeSuggestion timeSuggestion, PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            justVerifyTimeZoneWasSuggested(timeZoneSuggestion);
+            justVerifyTimeWasSuggested(timeSuggestion);
+            commitStateChanges();
+            return this;
+        }
+
+        private void justVerifyTimeWasNotSuggested() {
+            mFakeNewTimeServiceHelper.suggestedTimes.assertHasNotBeenSet();
+        }
+
+        private void justVerifyTimeZoneWasSuggested(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            mFakeNewTimeServiceHelper.suggestedTimeZones.assertHasBeenSet();
+            mFakeNewTimeServiceHelper.suggestedTimeZones.assertLatestEquals(timeZoneSuggestion);
+        }
+
+        private void justVerifyTimeWasSuggested(PhoneTimeSuggestion timeSuggestion) {
+            mFakeNewTimeServiceHelper.suggestedTimes.assertChangeCount(1);
+            mFakeNewTimeServiceHelper.suggestedTimes.assertLatestEquals(timeSuggestion);
+        }
+
+        private void commitStateChanges() {
+            mFakeNewTimeServiceHelper.commitState();
+        }
+    }
+
+    /** Some piece of state that tests want to track. */
+    private static class TestState<T> {
+        private T mInitialValue;
+        private LinkedList<T> mValues = new LinkedList<>();
+
+        void init(T value) {
+            mValues.clear();
+            mInitialValue = value;
+        }
+
+        void set(T value) {
+            mValues.addFirst(value);
+        }
+
+        boolean hasBeenSet() {
+            return mValues.size() > 0;
+        }
+
+        void assertHasNotBeenSet() {
+            assertFalse(hasBeenSet());
+        }
+
+        void assertHasBeenSet() {
+            assertTrue(hasBeenSet());
+        }
+
+        void commitLatest() {
+            if (hasBeenSet()) {
+                mInitialValue = mValues.getLast();
+                mValues.clear();
+            }
+        }
+
+        void assertLatestEquals(T expected) {
+            assertEquals(expected, getLatest());
+        }
+
+        void assertChangeCount(int expectedCount) {
+            assertEquals(expectedCount, mValues.size());
+        }
+
+        public T getLatest() {
+            if (hasBeenSet()) {
+                return mValues.getFirst();
+            }
+            return mInitialValue;
+        }
+    }
+
+    /**
+     * A fake implementation of {@link NewTimeServiceHelper} that enables tests to detect what
+     * {@link NewNitzStateMachineImpl} would do to a real device's state.
+     */
+    private static class FakeNewTimeServiceHelper implements NewTimeServiceHelper {
+
+        private final FakeDeviceState mFakeDeviceState;
+
+        // State we want to track.
+        public final TestState<PhoneTimeSuggestion> suggestedTimes = new TestState<>();
+        public final TestState<PhoneTimeZoneSuggestion> suggestedTimeZones = new TestState<>();
+
+        FakeNewTimeServiceHelper(FakeDeviceState fakeDeviceState) {
+            mFakeDeviceState = fakeDeviceState;
+        }
+
+        @Override
+        public void suggestDeviceTime(PhoneTimeSuggestion timeSuggestion) {
+            suggestedTimes.set(timeSuggestion);
+            // The fake time service just uses the latest suggestion.
+            mFakeDeviceState.currentTimeMillis = timeSuggestion.getUtcTime().getValue();
+        }
+
+        @Override
+        public void maybeSuggestDeviceTimeZone(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            suggestedTimeZones.set(timeZoneSuggestion);
+        }
+
+        @Override
+        public void dumpLogs(IndentingPrintWriter ipw) {
+            // No-op in tests
+        }
+
+        @Override
+        public void dumpState(PrintWriter pw) {
+            // No-op in tests
+        }
+
+        void commitState() {
+            suggestedTimeZones.commitLatest();
+            suggestedTimes.commitLatest();
+        }
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactoryTest.java b/tests/telephonytests/src/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactoryTest.java
new file mode 100644
index 0000000..3e69d23
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/nitz/NitzSignalInputFilterPredicateFactoryTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO1;
+import static com.android.internal.telephony.nitz.NitzSignalInputFilterPredicateFactory.createBogusElapsedRealtimeCheck;
+import static com.android.internal.telephony.nitz.NitzSignalInputFilterPredicateFactory.createIgnoreNitzPropertyCheck;
+import static com.android.internal.telephony.nitz.NitzSignalInputFilterPredicateFactory.createRateLimitCheck;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.util.TimestampedValue;
+
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachineTestSupport.FakeDeviceState;
+import com.android.internal.telephony.NitzStateMachineTestSupport.Scenario;
+import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.nitz.NitzSignalInputFilterPredicateFactory.NitzSignalInputFilterPredicateImpl;
+import com.android.internal.telephony.nitz.NitzSignalInputFilterPredicateFactory.TrivalentPredicate;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class NitzSignalInputFilterPredicateFactoryTest extends TelephonyTest {
+
+    private FakeDeviceState mFakeDeviceState;
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp("NitzSignalInputFilterPredicateFactoryTest");
+        mFakeDeviceState = new FakeDeviceState();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void testNitzSignalInputFilterPredicateImpl_nullSecondArgumentRejected() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        TrivalentPredicate[] triPredicates = {};
+        NitzSignalInputFilterPredicateImpl impl =
+                new NitzSignalInputFilterPredicateImpl(triPredicates);
+        try {
+            impl.mustProcessNitzSignal(nitzSignal, null);
+            fail();
+        } catch (NullPointerException expected) {
+        }
+    }
+
+    @Test
+    public void testNitzSignalInputFilterPredicateImpl_defaultIsTrue() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal = scenario
+                .createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        NitzSignalInputFilterPredicateImpl impl =
+                new NitzSignalInputFilterPredicateImpl(new TrivalentPredicate[0]);
+        assertTrue(impl.mustProcessNitzSignal(null, nitzSignal));
+    }
+
+    @Test
+    public void testNitzSignalInputFilterPredicateImpl_nullIsIgnored() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        TrivalentPredicate nullPredicate = (x, y) -> null;
+        TrivalentPredicate[] triPredicates = { nullPredicate };
+        NitzSignalInputFilterPredicateImpl impl =
+                new NitzSignalInputFilterPredicateImpl(triPredicates);
+        assertTrue(impl.mustProcessNitzSignal(null, nitzSignal));
+    }
+
+    @Test
+    public void testNitzSignalInputFilterPredicateImpl_trueIsHonored() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        TrivalentPredicate nullPredicate = (x, y) -> null;
+        TrivalentPredicate truePredicate = (x, y) -> true;
+        TrivalentPredicate exceptionPredicate = (x, y) -> {
+            throw new RuntimeException();
+        };
+        TrivalentPredicate[] triPredicates = {
+                nullPredicate,
+                truePredicate,
+                exceptionPredicate,
+        };
+        NitzSignalInputFilterPredicateImpl impl =
+                new NitzSignalInputFilterPredicateImpl(triPredicates);
+        assertTrue(impl.mustProcessNitzSignal(null, nitzSignal));
+    }
+
+    @Test
+    public void testNitzSignalInputFilterPredicateImpl_falseIsHonored() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+        TrivalentPredicate nullPredicate = (x, y) -> null;
+        TrivalentPredicate falsePredicate = (x, y) -> false;
+        TrivalentPredicate exceptionPredicate = (x, y) -> {
+            throw new RuntimeException();
+        };
+        TrivalentPredicate[] triPredicates = {
+                nullPredicate,
+                falsePredicate,
+                exceptionPredicate,
+        };
+        NitzSignalInputFilterPredicateImpl impl =
+                new NitzSignalInputFilterPredicateImpl(triPredicates);
+        assertFalse(impl.mustProcessNitzSignal(null, nitzSignal));
+    }
+
+    @Test
+    public void testTrivalentPredicate_ignoreNitzPropertyCheck() {
+        TrivalentPredicate triPredicate = createIgnoreNitzPropertyCheck(mFakeDeviceState);
+
+        mFakeDeviceState.ignoreNitz = true;
+        assertFalse(triPredicate.mustProcessNitzSignal(null, null));
+
+        mFakeDeviceState.ignoreNitz = false;
+        assertNull(triPredicate.mustProcessNitzSignal(null, null));
+    }
+
+    @Test
+    public void testTrivalentPredicate_bogusElapsedRealtimeCheck() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        long elapsedRealtimeClock = mFakeDeviceState.elapsedRealtime();
+        TimestampedValue<NitzData> nitzSignal = scenario.createNitzSignal(elapsedRealtimeClock);
+
+        TrivalentPredicate triPredicate =
+                createBogusElapsedRealtimeCheck(mContext, mFakeDeviceState);
+        assertNull(triPredicate.mustProcessNitzSignal(null, nitzSignal));
+
+        // Any signal that claims to be from the future must be rejected.
+        TimestampedValue<NitzData> bogusNitzSignal = new TimestampedValue<>(
+                elapsedRealtimeClock + 1, nitzSignal.getValue());
+        assertFalse(triPredicate.mustProcessNitzSignal(null, bogusNitzSignal));
+    }
+
+    @Test
+    public void testTrivalentPredicate_noOldSignalCheck() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        TrivalentPredicate triPredicate =
+                NitzSignalInputFilterPredicateFactory.createNoOldSignalCheck();
+        assertTrue(triPredicate.mustProcessNitzSignal(null, nitzSignal));
+        assertNull(triPredicate.mustProcessNitzSignal(nitzSignal, nitzSignal));
+    }
+
+    @Test
+    public void testTrivalentPredicate_rateLimitCheck_elapsedRealtime() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        int nitzSpacingThreshold = mFakeDeviceState.getNitzUpdateSpacingMillis();
+        NitzData baseNitzData = scenario.createNitzData();
+
+        TrivalentPredicate triPredicate = createRateLimitCheck(mFakeDeviceState);
+
+        long baseElapsedRealtimeMillis = mFakeDeviceState.elapsedRealtime();
+        TimestampedValue<NitzData> baseSignal =
+                new TimestampedValue<>(baseElapsedRealtimeMillis, baseNitzData);
+
+        // Two identical signals: no spacing so the new signal should not be processed.
+        {
+            assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, baseSignal));
+        }
+
+        // Two signals not spaced apart enough: the new signal should not processed.
+        {
+            int elapsedTimeIncrement = nitzSpacingThreshold - 1;
+            TimestampedValue<NitzData> newSignal =
+                    createIncrementedNitzSignal(baseSignal, elapsedTimeIncrement);
+            assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, newSignal));
+        }
+
+        // Two signals spaced apart: the new signal should be processed.
+        {
+            int elapsedTimeIncrement = nitzSpacingThreshold + 1;
+            TimestampedValue<NitzData> newSignal =
+                    createIncrementedNitzSignal(baseSignal, elapsedTimeIncrement);
+            assertTrue(triPredicate.mustProcessNitzSignal(baseSignal, newSignal));
+        }
+    }
+
+    @Test
+    public void testTrivalentPredicate_rateLimitCheck_offsetDifference() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        int nitzSpacingThreshold = mFakeDeviceState.getNitzUpdateSpacingMillis();
+        NitzData baseNitzData = scenario.createNitzData();
+
+        TrivalentPredicate triPredicate = createRateLimitCheck(mFakeDeviceState);
+
+        long baseElapsedRealtimeMillis = mFakeDeviceState.elapsedRealtime();
+        TimestampedValue<NitzData> baseSignal =
+                new TimestampedValue<>(baseElapsedRealtimeMillis, baseNitzData);
+
+        // Create a new NitzSignal that should be filtered.
+        int elapsedTimeIncrement = nitzSpacingThreshold - 1;
+        TimestampedValue<NitzData> intermediateNitzSignal =
+                createIncrementedNitzSignal(baseSignal, elapsedTimeIncrement);
+        NitzData intermediateNitzData = intermediateNitzSignal.getValue();
+        assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, intermediateNitzSignal));
+
+        // Two signals spaced apart so that the second would be filtered, but they contain different
+        // offset information so should be detected as "different" and processed.
+        {
+            // Modifying the local offset should be enough to recognize the NitzData as different.
+            NitzData differentOffsetNitzData = NitzData.createForTests(
+                    intermediateNitzData.getLocalOffsetMillis() + 1,
+                    intermediateNitzData.getDstAdjustmentMillis(),
+                    intermediateNitzData.getCurrentTimeInMillis(),
+                    intermediateNitzData.getEmulatorHostTimeZone());
+            TimestampedValue<NitzData> differentOffsetSignal = new TimestampedValue<>(
+                    baseSignal.getReferenceTimeMillis() + elapsedTimeIncrement,
+                    differentOffsetNitzData);
+            assertTrue(triPredicate.mustProcessNitzSignal(baseSignal, differentOffsetSignal));
+        }
+    }
+
+    @Test
+    public void testTrivalentPredicate_rateLimitCheck_utcTimeDifferences() {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        int nitzSpacingThreshold = mFakeDeviceState.getNitzUpdateSpacingMillis();
+        int nitzUtcDiffThreshold = mFakeDeviceState.getNitzUpdateDiffMillis();
+        NitzData baseNitzData = scenario.createNitzData();
+
+        TrivalentPredicate triPredicate = createRateLimitCheck(mFakeDeviceState);
+
+        long baseElapsedRealtimeMillis = mFakeDeviceState.elapsedRealtime();
+        TimestampedValue<NitzData> baseSignal =
+                new TimestampedValue<>(baseElapsedRealtimeMillis, baseNitzData);
+
+        // Create a new NitzSignal that should be filtered.
+        int elapsedTimeIncrement = nitzSpacingThreshold - 1;
+        TimestampedValue<NitzData> intermediateSignal =
+                createIncrementedNitzSignal(baseSignal, elapsedTimeIncrement);
+        NitzData intermediateNitzData = intermediateSignal.getValue();
+        assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, intermediateSignal));
+
+        // Two signals spaced apart so that the second would normally be filtered and it contains
+        // a UTC time that is not sufficiently different.
+        {
+            NitzData incrementedUtcTimeNitzData = NitzData.createForTests(
+                    intermediateNitzData.getLocalOffsetMillis(),
+                    intermediateNitzData.getDstAdjustmentMillis(),
+                    intermediateNitzData.getCurrentTimeInMillis() + nitzUtcDiffThreshold - 1,
+                    intermediateNitzData.getEmulatorHostTimeZone());
+
+            TimestampedValue<NitzData> incrementedNitzSignal = new TimestampedValue<>(
+                    intermediateSignal.getReferenceTimeMillis(), incrementedUtcTimeNitzData);
+            assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, incrementedNitzSignal));
+        }
+
+        // Two signals spaced apart so that the second would normally be filtered but it contains
+        // a UTC time that is sufficiently different.
+        {
+            NitzData incrementedUtcTimeNitzData = NitzData.createForTests(
+                    intermediateNitzData.getLocalOffsetMillis(),
+                    intermediateNitzData.getDstAdjustmentMillis(),
+                    intermediateNitzData.getCurrentTimeInMillis() + nitzUtcDiffThreshold + 1,
+                    intermediateNitzData.getEmulatorHostTimeZone());
+
+            TimestampedValue<NitzData> incrementedNitzSignal = new TimestampedValue<>(
+                    intermediateSignal.getReferenceTimeMillis(), incrementedUtcTimeNitzData);
+            assertTrue(triPredicate.mustProcessNitzSignal(baseSignal, incrementedNitzSignal));
+        }
+
+        // Two signals spaced apart so that the second would normally be filtered and it contains
+        // a UTC time that is not sufficiently different.
+        {
+            NitzData decrementedUtcTimeNitzData = NitzData.createForTests(
+                    intermediateNitzData.getLocalOffsetMillis(),
+                    intermediateNitzData.getDstAdjustmentMillis(),
+                    intermediateNitzData.getCurrentTimeInMillis() - nitzUtcDiffThreshold + 1,
+                    intermediateNitzData.getEmulatorHostTimeZone());
+
+            TimestampedValue<NitzData> decrementedNitzSignal = new TimestampedValue<>(
+                    intermediateSignal.getReferenceTimeMillis(), decrementedUtcTimeNitzData);
+            assertFalse(triPredicate.mustProcessNitzSignal(baseSignal, decrementedNitzSignal));
+        }
+
+        // Two signals spaced apart so that the second would normally be filtered but it contains
+        // a UTC time that is sufficiently different.
+        {
+            NitzData decrementedUtcTimeNitzData = NitzData.createForTests(
+                    intermediateNitzData.getLocalOffsetMillis(),
+                    intermediateNitzData.getDstAdjustmentMillis(),
+                    intermediateNitzData.getCurrentTimeInMillis() + nitzUtcDiffThreshold + 1,
+                    intermediateNitzData.getEmulatorHostTimeZone());
+
+            TimestampedValue<NitzData> decrementedNitzSignal = new TimestampedValue<>(
+                    intermediateSignal.getReferenceTimeMillis(), decrementedUtcTimeNitzData);
+            assertTrue(triPredicate.mustProcessNitzSignal(baseSignal, decrementedNitzSignal));
+        }
+    }
+
+    /**
+     * Creates an NITZ signal based on the the supplied signal but with all the fields related to
+     * elapsed time incremented by the specified number of milliseconds.
+     */
+    private static TimestampedValue<NitzData> createIncrementedNitzSignal(
+            TimestampedValue<NitzData> baseSignal, int incrementMillis) {
+        NitzData baseData = baseSignal.getValue();
+        return new TimestampedValue<>(baseSignal.getReferenceTimeMillis() + incrementMillis,
+                NitzData.createForTests(
+                        baseData.getLocalOffsetMillis(),
+                        baseData.getDstAdjustmentMillis(),
+                        baseData.getCurrentTimeInMillis() + incrementMillis,
+                        baseData.getEmulatorHostTimeZone()));
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/nitz/TimeZoneSuggesterImplTest.java b/tests/telephonytests/src/com/android/internal/telephony/nitz/TimeZoneSuggesterImplTest.java
new file mode 100644
index 0000000..cdd30e4
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/nitz/TimeZoneSuggesterImplTest.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz;
+
+import static com.android.internal.telephony.NitzStateMachineTestSupport.ARBITRARY_REALTIME_MILLIS;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.CZECHIA_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_COUNTRY_DEFAULT_ZONE_ID;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_DEFAULT_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NEW_ZEALAND_OTHER_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NON_UNIQUE_US_ZONE_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.NON_UNIQUE_US_ZONE_SCENARIO_ZONES;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO1;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNIQUE_US_ZONE_SCENARIO2;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.UNITED_KINGDOM_SCENARIO;
+import static com.android.internal.telephony.NitzStateMachineTestSupport.US_COUNTRY_DEFAULT_ZONE_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.util.TimestampedValue;
+
+import com.android.internal.telephony.NitzData;
+import com.android.internal.telephony.NitzStateMachineTestSupport.FakeDeviceState;
+import com.android.internal.telephony.NitzStateMachineTestSupport.Scenario;
+import com.android.internal.telephony.TelephonyTest;
+import com.android.internal.telephony.TimeZoneLookupHelper;
+import com.android.internal.telephony.nitz.NewNitzStateMachineImpl.TimeZoneSuggester;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class TimeZoneSuggesterImplTest extends TelephonyTest {
+
+    private static final int PHONE_ID = 99999;
+    private static final PhoneTimeZoneSuggestion EMPTY_TIME_ZONE_SUGGESTION =
+            new PhoneTimeZoneSuggestion(PHONE_ID);
+
+    private FakeDeviceState mFakeDeviceState;
+    private TimeZoneSuggester mTimeZoneSuggester;
+
+    @Before
+    public void setUp() throws Exception {
+        TelephonyTest.logd("TimeZoneSuggesterImplTest +Setup!");
+        super.setUp("TimeZoneSuggesterImplTest");
+
+        // In tests a fake impl is used for DeviceState, which allows historic data to be used.
+        mFakeDeviceState = new FakeDeviceState();
+
+        // In tests the real TimeZoneLookupHelper implementation is used: this makes it easy to
+        // construct tests using known historic examples.
+        TimeZoneLookupHelper timeZoneLookupHelper = new TimeZoneLookupHelper();
+        mTimeZoneSuggester = new TimeZoneSuggesterImpl(mFakeDeviceState, timeZoneLookupHelper);
+
+        TelephonyTest.logd("TimeZoneSuggesterImplTest -Setup!");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    @Test
+    public void test_emptySuggestionForNullCountryNullNitz() throws Exception {
+        assertEquals(EMPTY_TIME_ZONE_SUGGESTION,
+                mTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, null /* countryIsoCode */, null /* nitzSignal */));
+    }
+
+    @Test
+    public void test_emptySuggestionForNullCountryWithNitz() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+        TimestampedValue<NitzData> nitzSignal =
+                scenario.createNitzSignal(ARBITRARY_REALTIME_MILLIS);
+        assertEquals(EMPTY_TIME_ZONE_SUGGESTION,
+                mTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, null /* countryIsoCode */, nitzSignal));
+    }
+
+    @Test
+    public void test_emptySuggestionForEmptyCountryNullNitz() throws Exception {
+        assertEquals(EMPTY_TIME_ZONE_SUGGESTION,
+                mTimeZoneSuggester.getTimeZoneSuggestion(
+                        PHONE_ID, "" /* countryIsoCoe */, null /* nitzSignal */));
+    }
+
+    /**
+     * Tests behavior for various scenarios for a user in the US. The US is a complicated case
+     * with multiple time zones, some overlapping and with no good default. The scenario used here
+     * is a "unique" scenario, meaning it is possible to determine the correct zone using both
+     * country and NITZ information.
+     */
+    @Test
+    public void test_uniqueUsZone() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+
+        // Country won't be enough to get a quality result for time zone detection but a suggestion
+        // will be made.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(US_COUNTRY_DEFAULT_ZONE_ID);
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ with a "" country code is interpreted as a test network so only offset is used
+        // to get a match.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, "" /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(PHONE_ID, actualSuggestion.getPhoneId());
+            assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                    actualSuggestion.getMatchType());
+            assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                    actualSuggestion.getQuality());
+        }
+
+        // NITZ alone is not enough to get a result when the country is not available.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, null /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+
+        // Country + NITZ is enough for a unique time zone detection result for this scenario.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // Country + NITZ with a bad offset should not trigger fall back, country-only behavior
+        // since there are multiple zones to choose from.
+        {
+            // We use an NITZ from CZ to generate an NITZ signal with a bad offset.
+            TimestampedValue<NitzData> badNitzSignal =
+                    CZECHIA_SCENARIO.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion = EMPTY_TIME_ZONE_SUGGESTION;
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    badNitzSignal);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+    }
+
+    /**
+     * Tests behavior for various scenarios for a user in the US. The US is a complicated case
+     * with multiple time zones, some overlapping and with no good default. The scenario used here
+     * is a "non unique" scenario, meaning it is not possible to determine the a single zone using
+     * both country and NITZ information.
+     */
+    @Test
+    public void test_nonUniqueUsZone() throws Exception {
+        Scenario scenario = NON_UNIQUE_US_ZONE_SCENARIO;
+
+        // Country won't be enough to get a quality result for time zone detection but a suggestion
+        // will be made.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(US_COUNTRY_DEFAULT_ZONE_ID);
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ with a "" country code is interpreted as a test network so only offset is used
+        // to get a match.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, "" /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(PHONE_ID, actualSuggestion.getPhoneId());
+            assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                    actualSuggestion.getMatchType());
+            assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                    actualSuggestion.getQuality());
+        }
+
+        // NITZ alone is not enough to get a result when the country is not available.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, null /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+
+        // Country + NITZ is not enough for a unique time zone detection result for this scenario.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(PHONE_ID, actualSuggestion.getPhoneId());
+            assertEquals(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET,
+                    actualSuggestion.getMatchType());
+            assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                    actualSuggestion.getQuality());
+            List<String> allowedZoneIds = Arrays.asList(NON_UNIQUE_US_ZONE_SCENARIO_ZONES);
+            assertTrue(allowedZoneIds.contains(actualSuggestion.getZoneId()));
+        }
+
+        // Country + NITZ with a bad offset should not trigger fall back, country-only behavior
+        // since there are multiple zones to choose from.
+        {
+            // We use an NITZ from CZ to generate an NITZ signal with a bad offset.
+            TimestampedValue<NitzData> badNitzSignal =
+                    CZECHIA_SCENARIO.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion = EMPTY_TIME_ZONE_SUGGESTION;
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    badNitzSignal);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+    }
+
+    /**
+     * Tests behavior for various scenarios for a user in the UK. The UK is simple: it has a single
+     * time zone so only the country needs to be known to find a time zone. It is special in that
+     * it uses UTC for some of the year, which makes it difficult to detect bogus NITZ signals with
+     * zero'd offset information.
+     */
+    @Test
+    public void test_unitedKingdom() throws Exception {
+        Scenario scenario = UNITED_KINGDOM_SCENARIO;
+
+        // Country alone is enough to guess the time zone.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ with a "" country code is interpreted as a test network so only offset is used
+        // to get a match.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, "" /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(PHONE_ID, actualSuggestion.getPhoneId());
+            assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                    actualSuggestion.getMatchType());
+            assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                    actualSuggestion.getQuality());
+
+        }
+
+        // NITZ alone is not enough to get a result when the country is not available.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, null /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+
+        // Country + NITZ is enough for both time + time zone detection.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // Country + NITZ with a bad offset should trigger fall back, country-only behavior since
+        // there's only one zone.
+        {
+            // We use an NITZ from Czechia to generate an NITZ signal with a bad offset.
+            TimestampedValue<NitzData> badNitzSignal =
+                    CZECHIA_SCENARIO.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    badNitzSignal);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+    }
+
+    /**
+     * Tests behavior for various scenarios for a user in Czechia. CZ is simple: it has a single
+     * time zone so only the country needs to be known to find a time zone. It never uses UTC so it
+     * is useful to contrast with the UK and can be used for bogus signal detection.
+     */
+    @Test
+    public void test_cz() throws Exception {
+        Scenario scenario = CZECHIA_SCENARIO;
+
+        // Country alone is enough to guess the time zone.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ with a "" country code is interpreted as a test network so only offset is used
+        // to get a match.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion =
+                    mTimeZoneSuggester.getTimeZoneSuggestion(
+                            PHONE_ID, "" /* countryIsoCode */,
+                            scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(PHONE_ID, actualSuggestion.getPhoneId());
+            assertEquals(PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY,
+                    actualSuggestion.getMatchType());
+            assertEquals(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET,
+                    actualSuggestion.getQuality());
+
+        }
+
+        // NITZ alone is not enough to get a result when the country is not available.
+        {
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, null /* countryIsoCode */,
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+
+        // Country + NITZ is enough for both time + time zone detection.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime()));
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // Country + NITZ with a bad offset should trigger fall back, country-only behavior since
+        // there's only one zone.
+        {
+            // We use an NITZ from the US to generate an NITZ signal with a bad offset.
+            TimestampedValue<NitzData> badNitzSignal =
+                    UNIQUE_US_ZONE_SCENARIO1.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(),
+                    badNitzSignal);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+    }
+
+    @Test
+    public void test_bogusCzNitzSignal() throws Exception {
+        Scenario scenario = CZECHIA_SCENARIO;
+
+        // Country alone is enough to guess the time zone.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.SINGLE_ZONE);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ + bogus NITZ is not enough to get a result.
+        {
+            // Create a corrupted NITZ signal, where the offset information has been lost.
+            TimestampedValue<NitzData> goodNitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            NitzData bogusNitzData = NitzData.createForTests(
+                    0 /* UTC! */, null /* dstOffsetMillis */,
+                    goodNitzSignal.getValue().getCurrentTimeInMillis(),
+                    null /* emulatorHostTimeZone */);
+            TimestampedValue<NitzData> badNitzSignal = new TimestampedValue<>(
+                    goodNitzSignal.getReferenceTimeMillis(), bogusNitzData);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), badNitzSignal);
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+    }
+
+    @Test
+    public void test_bogusUniqueUsNitzSignal() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+
+        // Country alone is not enough to guess the time zone.
+        {
+            PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                    new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedTimeZoneSuggestion.setZoneId(US_COUNTRY_DEFAULT_ZONE_ID);
+            expectedTimeZoneSuggestion.setMatchType(
+                    PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedTimeZoneSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+        }
+
+        // NITZ + bogus NITZ is not enough to get a result.
+        {
+            // Create a corrupted NITZ signal, where the offset information has been lost.
+            TimestampedValue<NitzData> goodNitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            NitzData bogusNitzData = NitzData.createForTests(
+                    0 /* UTC! */, null /* dstOffsetMillis */,
+                    goodNitzSignal.getValue().getCurrentTimeInMillis(),
+                    null /* emulatorHostTimeZone */);
+            TimestampedValue<NitzData> badNitzSignal = new TimestampedValue<>(
+                    goodNitzSignal.getReferenceTimeMillis(), bogusNitzData);
+
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), badNitzSignal);
+            assertEquals(EMPTY_TIME_ZONE_SUGGESTION, actualSuggestion);
+        }
+    }
+
+    @Test
+    public void test_emulatorNitzExtensionUsedForTimeZone() throws Exception {
+        Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+
+        TimestampedValue<NitzData> originalNitzSignal =
+                scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+
+        // Create an NITZ signal with an explicit time zone (as can happen on emulators).
+        NitzData originalNitzData = originalNitzSignal.getValue();
+
+        // A time zone that is obviously not in the US, but because the explicit value is present it
+        // should not be questioned.
+        String emulatorTimeZoneId = "Europe/London";
+        NitzData emulatorNitzData = NitzData.createForTests(
+                originalNitzData.getLocalOffsetMillis(),
+                originalNitzData.getDstAdjustmentMillis(),
+                originalNitzData.getCurrentTimeInMillis(),
+                java.util.TimeZone.getTimeZone(emulatorTimeZoneId) /* emulatorHostTimeZone */);
+        TimestampedValue<NitzData> emulatorNitzSignal = new TimestampedValue<>(
+                originalNitzSignal.getReferenceTimeMillis(), emulatorNitzData);
+
+        PhoneTimeZoneSuggestion expectedTimeZoneSuggestion =
+                new PhoneTimeZoneSuggestion(PHONE_ID);
+        expectedTimeZoneSuggestion.setZoneId(emulatorTimeZoneId);
+        expectedTimeZoneSuggestion.setMatchType(PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID);
+        expectedTimeZoneSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+
+        PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                PHONE_ID, scenario.getNetworkCountryIsoCode(), emulatorNitzSignal);
+        assertEquals(expectedTimeZoneSuggestion, actualSuggestion);
+    }
+
+    @Test
+    public void test_countryDefaultBoost() throws Exception {
+        // Demonstrate the defaultTimeZoneBoost behavior: we can get a zone only from the
+        // countryIsoCode.
+        {
+            Scenario scenario = NEW_ZEALAND_DEFAULT_SCENARIO;
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(NEW_ZEALAND_COUNTRY_DEFAULT_ZONE_ID);
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // Confirm what happens when NITZ is correct for the country default.
+        {
+            Scenario scenario = NEW_ZEALAND_DEFAULT_SCENARIO;
+            TimestampedValue<NitzData> nitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // A valid NITZ signal for the non-default zone should still be correctly detected.
+        {
+            Scenario scenario = NEW_ZEALAND_OTHER_SCENARIO;
+            TimestampedValue<NitzData> nitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // Demonstrate what happens with a bogus NITZ for NZ: because the default zone is boosted
+        // then we should return to the country default zone.
+        {
+            Scenario scenario = NEW_ZEALAND_DEFAULT_SCENARIO;
+            // Use a scenario that has a different offset than NZ to generate the NITZ signal.
+            TimestampedValue<NitzData> nitzSignal =
+                    CZECHIA_SCENARIO.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(NEW_ZEALAND_COUNTRY_DEFAULT_ZONE_ID);
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+    }
+
+    @Test
+    public void test_noCountryDefaultBoost() throws Exception {
+        // Demonstrate the behavior without default country boost for a country with multiple zones:
+        // we cannot get a zone only from the countryIsoCode.
+        {
+            Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(US_COUNTRY_DEFAULT_ZONE_ID);
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+            expectedSuggestion.setQuality(
+                    PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), null /* nitzSignal */);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // Confirm what happens when NITZ is correct for the country default.
+        {
+            Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+            TimestampedValue<NitzData> nitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // A valid NITZ signal for the non-default zone should still be correctly detected.
+        {
+            Scenario scenario = UNIQUE_US_ZONE_SCENARIO2;
+            TimestampedValue<NitzData> nitzSignal =
+                    scenario.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = new PhoneTimeZoneSuggestion(PHONE_ID);
+            expectedSuggestion.setZoneId(scenario.getTimeZoneId());
+            expectedSuggestion.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET);
+            expectedSuggestion.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+
+        // Demonstrate what happens with a bogus NITZ for US: because the default zone is not
+        // boosted we should not get a suggestion.
+        {
+            // A scenario that has a different offset than US.
+            Scenario scenario = UNIQUE_US_ZONE_SCENARIO1;
+            // Use a scenario that has a different offset than the US to generate the NITZ signal.
+            TimestampedValue<NitzData> nitzSignal =
+                    CZECHIA_SCENARIO.createNitzSignal(mFakeDeviceState.elapsedRealtime());
+            PhoneTimeZoneSuggestion expectedSuggestion = EMPTY_TIME_ZONE_SUGGESTION;
+            PhoneTimeZoneSuggestion actualSuggestion = mTimeZoneSuggester.getTimeZoneSuggestion(
+                    PHONE_ID, scenario.getNetworkCountryIsoCode(), nitzSignal);
+            assertEquals(expectedSuggestion, actualSuggestion);
+        }
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestionTest.java b/tests/telephonytests/src/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestionTest.java
new file mode 100644
index 0000000..54838ae
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/nitz/service/PhoneTimeZoneSuggestionTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz.service;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.junit.Test;
+
+public class PhoneTimeZoneSuggestionTest {
+    private static final int PHONE_ID = 99999;
+
+    @Test
+    public void testEquals() {
+        PhoneTimeZoneSuggestion one = new PhoneTimeZoneSuggestion(PHONE_ID);
+        assertEquals(one, one);
+
+        PhoneTimeZoneSuggestion two = new PhoneTimeZoneSuggestion(PHONE_ID);
+        assertEquals(one, two);
+        assertEquals(two, one);
+
+        PhoneTimeZoneSuggestion three = new PhoneTimeZoneSuggestion(PHONE_ID + 1);
+        assertNotEquals(one, three);
+        assertNotEquals(three, one);
+
+        one.setZoneId("Europe/London");
+        assertNotEquals(one, two);
+        two.setZoneId("Europe/Paris");
+        assertNotEquals(one, two);
+        one.setZoneId(two.getZoneId());
+        assertEquals(one, two);
+
+        one.setMatchType(PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID);
+        two.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+        assertNotEquals(one, two);
+        one.setMatchType(PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY);
+        assertEquals(one, two);
+
+        one.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+        two.setQuality(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+        assertNotEquals(one, two);
+        one.setQuality(PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS);
+        assertEquals(one, two);
+
+        // DebugInfo must not be considered in equals().
+        one.addDebugInfo("Debug info 1");
+        two.addDebugInfo("Debug info 2");
+        assertEquals(one, two);
+    }
+
+    @Test
+    public void testParcelable() {
+        PhoneTimeZoneSuggestion one = new PhoneTimeZoneSuggestion(PHONE_ID);
+        assertEquals(one, roundTripParcelable(one));
+
+        one.setZoneId("Europe/London");
+        one.setMatchType(PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID);
+        one.setQuality(PhoneTimeZoneSuggestion.SINGLE_ZONE);
+        assertEquals(one, roundTripParcelable(one));
+
+        // DebugInfo should also be stored (but is not checked by equals()
+        one.addDebugInfo("This is debug info");
+        PhoneTimeZoneSuggestion two = roundTripParcelable(one);
+        assertEquals(one.getDebugInfo(), two.getDebugInfo());
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T extends Parcelable> T roundTripParcelable(T one) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeTypedObject(one, 0);
+        parcel.setDataPosition(0);
+
+        T toReturn = (T) parcel.readTypedObject(PhoneTimeZoneSuggestion.CREATOR);
+        parcel.recycle();
+        return toReturn;
+    }
+}
diff --git a/tests/telephonytests/src/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceTest.java b/tests/telephonytests/src/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceTest.java
new file mode 100644
index 0000000..f268501
--- /dev/null
+++ b/tests/telephonytests/src/com/android/internal/telephony/nitz/service/TimeZoneDetectionServiceTest.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright 2019 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.internal.telephony.nitz.service;
+
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.EMULATOR_ZONE_ID;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MULTIPLE_ZONES_WITH_SAME_OFFSET;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.NETWORK_COUNTRY_AND_OFFSET;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.NETWORK_COUNTRY_ONLY;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.SINGLE_ZONE;
+import static com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.TEST_NETWORK_OFFSET_ONLY;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_HIGH;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_HIGHEST;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_LOW;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_MEDIUM;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_NONE;
+import static com.android.internal.telephony.nitz.service.TimeZoneDetectionService.SCORE_USAGE_THRESHOLD;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.MatchType;
+import com.android.internal.telephony.nitz.service.PhoneTimeZoneSuggestion.Quality;
+import com.android.internal.telephony.nitz.service.TimeZoneDetectionService.QualifiedPhoneTimeZoneSuggestion;
+import com.android.internal.util.IndentingPrintWriter;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+
+/**
+ * White-box unit tests for {@link TimeZoneDetectionService}.
+ */
+public class TimeZoneDetectionServiceTest {
+
+    private static final int PHONE1_ID = 10000;
+    private static final int PHONE2_ID = 20000;
+
+    // Suggestion test cases are ordered so that each successive one is of the same or higher score
+    // than the previous.
+    private static final SuggestionTestCase[] TEST_CASES = new SuggestionTestCase[] {
+            newTestCase(NETWORK_COUNTRY_ONLY, MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW),
+            newTestCase(NETWORK_COUNTRY_ONLY, MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_MEDIUM),
+            newTestCase(NETWORK_COUNTRY_AND_OFFSET, MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_MEDIUM),
+            newTestCase(NETWORK_COUNTRY_ONLY, SINGLE_ZONE, SCORE_HIGH),
+            newTestCase(NETWORK_COUNTRY_AND_OFFSET, SINGLE_ZONE, SCORE_HIGH),
+            newTestCase(TEST_NETWORK_OFFSET_ONLY, MULTIPLE_ZONES_WITH_SAME_OFFSET, SCORE_HIGHEST),
+            newTestCase(EMULATOR_ZONE_ID, SINGLE_ZONE, SCORE_HIGHEST),
+    };
+
+    private TimeZoneDetectionService mTimeZoneDetectionService;
+    private FakeTimeZoneDetectionServiceHelper mFakeTimeZoneDetectionServiceHelper;
+
+    @Before
+    public void setUp() {
+        mFakeTimeZoneDetectionServiceHelper = new FakeTimeZoneDetectionServiceHelper();
+        mTimeZoneDetectionService =
+                new TimeZoneDetectionService(mFakeTimeZoneDetectionServiceHelper);
+    }
+
+    @Test
+    public void testEmptySuggestions() {
+        PhoneTimeZoneSuggestion phone1TimeZoneSuggestion = createEmptyPhone1Suggestion();
+        PhoneTimeZoneSuggestion phone2TimeZoneSuggestion = createEmptyPhone2Suggestion();
+        Script script = new Script()
+                .initializeTimeZoneDetectionEnabled(true)
+                .initializeTimeZoneSetting(true);
+
+        script.suggestPhoneTimeZone(phone1TimeZoneSuggestion)
+                .verifyTimeZoneNotSet();
+
+        // Assert internal service state.
+        QualifiedPhoneTimeZoneSuggestion expectedPhone1ScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(phone1TimeZoneSuggestion, SCORE_NONE);
+        assertEquals(expectedPhone1ScoredSuggestion,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+        assertNull(mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+        assertEquals(expectedPhone1ScoredSuggestion,
+                mTimeZoneDetectionService.findBestSuggestionForTests());
+
+        script.suggestPhoneTimeZone(phone2TimeZoneSuggestion)
+                .verifyTimeZoneNotSet();
+
+        // Assert internal service state.
+        QualifiedPhoneTimeZoneSuggestion expectedPhone2ScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(phone2TimeZoneSuggestion, SCORE_NONE);
+        assertEquals(expectedPhone1ScoredSuggestion,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+        assertEquals(expectedPhone2ScoredSuggestion,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+        // Phone 1 should always beat phone 2, all other things being equal.
+        assertEquals(expectedPhone1ScoredSuggestion,
+                mTimeZoneDetectionService.findBestSuggestionForTests());
+    }
+
+    @Test
+    public void testFirstPlausibleSuggestionAcceptedWhenTimeZoneUninitialized() {
+        SuggestionTestCase testCase =
+                newTestCase(NETWORK_COUNTRY_ONLY, MULTIPLE_ZONES_WITH_DIFFERENT_OFFSETS, SCORE_LOW);
+        PhoneTimeZoneSuggestion lowQualitySuggestion =
+                testCase.createSuggestion(PHONE1_ID, "America/New_York");
+        Script script = new Script()
+                .initializeTimeZoneDetectionEnabled(true);
+
+        // The device is uninitialized.
+        script.initializeTimeZoneSetting(false);
+
+        // The very first suggestion will be taken.
+        script.suggestPhoneTimeZone(lowQualitySuggestion)
+                .verifyTimeZoneSetAndReset(lowQualitySuggestion);
+
+        // Assert internal service state.
+        QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion, testCase.expectedScore);
+        assertEquals(expectedScoredSuggestion,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+        assertEquals(expectedScoredSuggestion,
+                mTimeZoneDetectionService.findBestSuggestionForTests());
+
+        // Another low quality suggestion will be ignored now that the setting is initialized.
+        PhoneTimeZoneSuggestion lowQualitySuggestion2 =
+                testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
+        script.suggestPhoneTimeZone(lowQualitySuggestion2)
+                .verifyTimeZoneNotSet();
+
+        // Assert internal service state.
+        QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion2 =
+                new QualifiedPhoneTimeZoneSuggestion(lowQualitySuggestion2, testCase.expectedScore);
+        assertEquals(expectedScoredSuggestion2,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+        assertEquals(expectedScoredSuggestion2,
+                mTimeZoneDetectionService.findBestSuggestionForTests());
+    }
+
+    @Test
+    public void testTogglingTimeZoneDetection() {
+        Script script = new Script()
+                .initializeTimeZoneSetting(true);
+
+        boolean timeZoneDetectionEnabled = false;
+        script.initializeTimeZoneDetectionEnabled(timeZoneDetectionEnabled);
+
+        for (int i = 0; i < TEST_CASES.length; i++) {
+            SuggestionTestCase testCase = TEST_CASES[i];
+
+            PhoneTimeZoneSuggestion suggestion =
+                    testCase.createSuggestion(PHONE1_ID, "Europe/London");
+            script.suggestPhoneTimeZone(suggestion);
+
+            // When time zone detection is already enabled the suggestion (if it scores highly
+            // enough) should be set immediately.
+            if (timeZoneDetectionEnabled) {
+                if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+                    script.verifyTimeZoneSetAndReset(suggestion);
+                } else {
+                    script.verifyTimeZoneNotSet();
+                }
+            } else {
+                script.verifyTimeZoneNotSet();
+            }
+
+            // Assert internal service state.
+            QualifiedPhoneTimeZoneSuggestion expectedScoredSuggestion =
+                    new QualifiedPhoneTimeZoneSuggestion(suggestion, testCase.expectedScore);
+            assertEquals(expectedScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedScoredSuggestion,
+                    mTimeZoneDetectionService.findBestSuggestionForTests());
+
+            // Now toggle the time zone detection setting: when it is toggled to on and the most
+            // recent suggestion scores highly enough, the time zone should be set.
+            timeZoneDetectionEnabled = !timeZoneDetectionEnabled;
+            script.timeZoneDetectionEnabled(timeZoneDetectionEnabled);
+            if (timeZoneDetectionEnabled) {
+                if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+                    script.verifyTimeZoneSetAndReset(suggestion);
+                } else {
+                    script.verifyTimeZoneNotSet();
+                }
+            } else {
+                script.verifyTimeZoneNotSet();
+            }
+
+            // Assert internal service state.
+            assertEquals(expectedScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedScoredSuggestion,
+                    mTimeZoneDetectionService.findBestSuggestionForTests());
+        }
+    }
+
+    @Test
+    public void testSuggestionsSinglePhone() {
+        Script script = new Script()
+                .initializeTimeZoneDetectionEnabled(true)
+                .initializeTimeZoneSetting(true);
+
+        for (SuggestionTestCase testCase : TEST_CASES) {
+            makePhone1SuggestionAndCheckState(script, testCase);
+        }
+
+        /*
+         * This is the same test as above but the test cases are in
+         * reverse order of their expected score. New suggestions always replace previous ones:
+         * there's effectively no history and so ordering shouldn't make any difference.
+         */
+
+        // Each test case will have the same or lower score than the last.
+        ArrayList<SuggestionTestCase> descendingCasesByScore =
+                new ArrayList<>(Arrays.asList(TEST_CASES));
+        Collections.reverse(descendingCasesByScore);
+
+        for (SuggestionTestCase testCase : descendingCasesByScore) {
+            makePhone1SuggestionAndCheckState(script, testCase);
+        }
+    }
+
+    private void makePhone1SuggestionAndCheckState(Script script, SuggestionTestCase testCase) {
+        String zoneId = "Europe/London";
+        PhoneTimeZoneSuggestion zonePhone1Suggestion = testCase.createSuggestion(PHONE1_ID, zoneId);
+        QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion, testCase.expectedScore);
+
+        script.suggestPhoneTimeZone(zonePhone1Suggestion);
+        if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+            script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
+        } else {
+            script.verifyTimeZoneNotSet();
+        }
+
+        // Assert internal service state.
+        assertEquals(expectedZonePhone1ScoredSuggestion,
+                mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+        assertEquals(expectedZonePhone1ScoredSuggestion,
+                mTimeZoneDetectionService.findBestSuggestionForTests());
+    }
+
+    /**
+     * Tries a set of test cases to see if the phone with the lowest ID is given preference. This
+     * test also confirms that the time zone setting would only be set if a suggestion is of
+     * sufficient quality.
+     */
+    @Test
+    public void testMultiplePhoneSuggestionScoringAndPhoneIdBias() {
+        String[] zoneIds = { "Europe/London", "Europe/Paris" };
+        PhoneTimeZoneSuggestion emptyPhone1Suggestion = createEmptyPhone1Suggestion();
+        PhoneTimeZoneSuggestion emptyPhone2Suggestion = createEmptyPhone2Suggestion();
+        QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone1ScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(emptyPhone1Suggestion, SCORE_NONE);
+        QualifiedPhoneTimeZoneSuggestion expectedEmptyPhone2ScoredSuggestion =
+                new QualifiedPhoneTimeZoneSuggestion(emptyPhone2Suggestion, SCORE_NONE);
+
+        Script script = new Script()
+                .initializeTimeZoneDetectionEnabled(true)
+                .initializeTimeZoneSetting(true)
+                // Initialize the latest suggestions as empty so we don't need to worry about nulls
+                // below for the first loop.
+                .suggestPhoneTimeZone(emptyPhone1Suggestion)
+                .suggestPhoneTimeZone(emptyPhone2Suggestion)
+                .resetState();
+
+        for (SuggestionTestCase testCase : TEST_CASES) {
+            PhoneTimeZoneSuggestion zonePhone1Suggestion =
+                    testCase.createSuggestion(PHONE1_ID, zoneIds[0]);
+            PhoneTimeZoneSuggestion zonePhone2Suggestion =
+                    testCase.createSuggestion(PHONE2_ID, zoneIds[1]);
+            QualifiedPhoneTimeZoneSuggestion expectedZonePhone1ScoredSuggestion =
+                    new QualifiedPhoneTimeZoneSuggestion(zonePhone1Suggestion,
+                            testCase.expectedScore);
+            QualifiedPhoneTimeZoneSuggestion expectedZonePhone2ScoredSuggestion =
+                    new QualifiedPhoneTimeZoneSuggestion(zonePhone2Suggestion,
+                            testCase.expectedScore);
+
+            // Start the test by making a suggestion for phone 1.
+            script.suggestPhoneTimeZone(zonePhone1Suggestion);
+            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+                script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
+            } else {
+                script.verifyTimeZoneNotSet();
+            }
+
+            // Assert internal service state.
+            assertEquals(expectedZonePhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedEmptyPhone2ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+            assertEquals(expectedZonePhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.findBestSuggestionForTests());
+
+            // Phone 2 then makes an identical suggestion. Phone 1's suggestion should still "win"
+            // if it is above the required threshold.
+            script.suggestPhoneTimeZone(zonePhone2Suggestion);
+            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+                script.verifyTimeZoneSetAndReset(zonePhone1Suggestion);
+            } else {
+                script.verifyTimeZoneNotSet();
+            }
+
+            // Assert internal service state.
+            assertEquals(expectedZonePhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedZonePhone2ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+            // Phone 1 should always beat phone 2, all other things being equal.
+            assertEquals(expectedZonePhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.findBestSuggestionForTests());
+
+            // Withdrawing phone 1's suggestion should leave phone 2 as the new winner. Since the
+            // zoneId is different, the time zone setting should be updated.
+            script.suggestPhoneTimeZone(emptyPhone1Suggestion);
+            if (testCase.expectedScore >= SCORE_USAGE_THRESHOLD) {
+                script.verifyTimeZoneSetAndReset(zonePhone2Suggestion);
+            } else {
+                script.verifyTimeZoneNotSet();
+            }
+
+            // Assert internal service state.
+            assertEquals(expectedEmptyPhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedZonePhone2ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+            assertEquals(expectedZonePhone2ScoredSuggestion,
+                    mTimeZoneDetectionService.findBestSuggestionForTests());
+
+            // Reset the state for the next loop.
+            script.suggestPhoneTimeZone(emptyPhone2Suggestion)
+                    .verifyTimeZoneNotSet();
+            assertEquals(expectedEmptyPhone1ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE1_ID));
+            assertEquals(expectedEmptyPhone2ScoredSuggestion,
+                    mTimeZoneDetectionService.getLatestPhoneSuggestion(PHONE2_ID));
+        }
+    }
+
+    /**
+     * The {@link TimeZoneDetectionService.Helper} is left to detect whether changing the the time
+     * zone is actually necessary. This test proves that the service doesn't assume it knows the
+     * current setting.
+     */
+    @Test
+    public void testTimeZoneDetectionServiceDoesNotAssumeCurrentSetting() {
+        Script script = new Script()
+                .initializeTimeZoneDetectionEnabled(true);
+
+        SuggestionTestCase testCase =
+                newTestCase(NETWORK_COUNTRY_AND_OFFSET, SINGLE_ZONE, SCORE_HIGH);
+        PhoneTimeZoneSuggestion losAngelesSuggestion =
+                testCase.createSuggestion(PHONE1_ID, "America/Los_Angeles");
+        PhoneTimeZoneSuggestion newYorkSuggestion =
+                testCase.createSuggestion(PHONE1_ID, "America/New_York");
+
+        // Initialization.
+        script.suggestPhoneTimeZone(losAngelesSuggestion)
+                .verifyTimeZoneSetAndReset(losAngelesSuggestion);
+        // Suggest it again - it should be set.
+        script.suggestPhoneTimeZone(losAngelesSuggestion)
+                .verifyTimeZoneSetAndReset(losAngelesSuggestion);
+
+        // Toggling time zone detection should set it.
+        script.timeZoneDetectionEnabled(false)
+                .timeZoneDetectionEnabled(true)
+                .verifyTimeZoneSetAndReset(losAngelesSuggestion);
+
+        // Simulate a user turning detection off, a new suggestion being made, and the user turning
+        // it on again.
+        script.timeZoneDetectionEnabled(false)
+                .suggestPhoneTimeZone(newYorkSuggestion)
+                .verifyTimeZoneNotSet();
+        // Latest suggestion should be used.
+        script.timeZoneDetectionEnabled(true)
+                .verifyTimeZoneSetAndReset(newYorkSuggestion);
+    }
+
+    private static PhoneTimeZoneSuggestion createEmptyPhone1Suggestion() {
+        return new PhoneTimeZoneSuggestion(PHONE1_ID);
+    }
+
+    private static PhoneTimeZoneSuggestion createEmptyPhone2Suggestion() {
+        return new PhoneTimeZoneSuggestion(PHONE2_ID);
+    }
+
+    class FakeTimeZoneDetectionServiceHelper implements TimeZoneDetectionService.Helper {
+
+        private Listener mListener;
+        private boolean mTimeZoneDetectionEnabled;
+        private boolean mTimeZoneInitialized = false;
+        private TestState<PhoneTimeZoneSuggestion> mTimeZoneSuggestion = new TestState<>();
+
+        @Override
+        public void setListener(Listener listener) {
+            this.mListener = listener;
+        }
+
+        @Override
+        public boolean isTimeZoneDetectionEnabled() {
+            return mTimeZoneDetectionEnabled;
+        }
+
+        @Override
+        public boolean isTimeZoneSettingInitialized() {
+            return mTimeZoneInitialized;
+        }
+
+        @Override
+        public void setDeviceTimeZoneFromSuggestion(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            mTimeZoneInitialized = true;
+            mTimeZoneSuggestion.set(timeZoneSuggestion);
+        }
+
+        @Override
+        public void dumpState(PrintWriter pw) {
+            // No-op for fake
+        }
+
+        @Override
+        public void dumpLogs(IndentingPrintWriter ipw) {
+            // No-op for fake
+        }
+
+        void initializeTimeZoneDetectionEnabled(boolean enabled) {
+            mTimeZoneDetectionEnabled = enabled;
+        }
+
+        void initializeTimeZone(boolean initialized) {
+            mTimeZoneInitialized = initialized;
+        }
+
+        void simulateTimeZoneDetectionEnabled(boolean enabled) {
+            mTimeZoneDetectionEnabled = enabled;
+            mListener.onTimeZoneDetectionChange(enabled);
+        }
+
+        void assertTimeZoneNotSet() {
+            mTimeZoneSuggestion.assertHasNotBeenSet();
+        }
+
+        void assertTimeZoneSuggested(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            mTimeZoneSuggestion.assertHasBeenSet();
+            mTimeZoneSuggestion.assertChangeCount(1);
+            mTimeZoneSuggestion.assertLatestEquals(timeZoneSuggestion);
+        }
+
+        void commitAllChanges() {
+            mTimeZoneSuggestion.commitLatest();
+        }
+    }
+
+    /** Some piece of state that tests want to track. */
+    private static class TestState<T> {
+        private T mInitialValue;
+        private LinkedList<T> mValues = new LinkedList<>();
+
+        void init(T value) {
+            mValues.clear();
+            mInitialValue = value;
+        }
+
+        void set(T value) {
+            mValues.addFirst(value);
+        }
+
+        boolean hasBeenSet() {
+            return mValues.size() > 0;
+        }
+
+        void assertHasNotBeenSet() {
+            assertFalse(hasBeenSet());
+        }
+
+        void assertHasBeenSet() {
+            assertTrue(hasBeenSet());
+        }
+
+        void commitLatest() {
+            if (hasBeenSet()) {
+                mInitialValue = mValues.getLast();
+                mValues.clear();
+            }
+        }
+
+        void assertLatestEquals(T expected) {
+            assertEquals(expected, getLatest());
+        }
+
+        void assertChangeCount(int expectedCount) {
+            assertEquals(expectedCount, mValues.size());
+        }
+
+        public T getLatest() {
+            if (hasBeenSet()) {
+                return mValues.getFirst();
+            }
+            return mInitialValue;
+        }
+    }
+
+    /**
+     * A "fluent" class allows reuse of code in tests: initialization, simulation and verification
+     * logic.
+     */
+    private class Script {
+
+        Script initializeTimeZoneDetectionEnabled(boolean enabled) {
+            mFakeTimeZoneDetectionServiceHelper.initializeTimeZoneDetectionEnabled(enabled);
+            return this;
+        }
+
+        Script initializeTimeZoneSetting(boolean initialized) {
+            mFakeTimeZoneDetectionServiceHelper.initializeTimeZone(initialized);
+            return this;
+        }
+
+        Script timeZoneDetectionEnabled(boolean timeZoneDetectionEnabled) {
+            mFakeTimeZoneDetectionServiceHelper.simulateTimeZoneDetectionEnabled(
+                    timeZoneDetectionEnabled);
+            return this;
+        }
+
+        /** Simulates the time zone detection service receiving a phone-originated suggestion. */
+        Script suggestPhoneTimeZone(PhoneTimeZoneSuggestion phoneTimeZoneSuggestion) {
+            mTimeZoneDetectionService.suggestPhoneTimeZone(phoneTimeZoneSuggestion);
+            return this;
+        }
+
+        Script verifyTimeZoneNotSet() {
+            mFakeTimeZoneDetectionServiceHelper.assertTimeZoneNotSet();
+            return this;
+        }
+
+        Script verifyTimeZoneSetAndReset(PhoneTimeZoneSuggestion timeZoneSuggestion) {
+            mFakeTimeZoneDetectionServiceHelper.assertTimeZoneSuggested(timeZoneSuggestion);
+            mFakeTimeZoneDetectionServiceHelper.commitAllChanges();
+            return this;
+        }
+
+        Script resetState() {
+            mFakeTimeZoneDetectionServiceHelper.commitAllChanges();
+            return this;
+        }
+    }
+
+    private static class SuggestionTestCase {
+        public final int matchType;
+        public final int quality;
+        public final int expectedScore;
+
+        SuggestionTestCase(int matchType, int quality, int expectedScore) {
+            this.matchType = matchType;
+            this.quality = quality;
+            this.expectedScore = expectedScore;
+        }
+
+        private PhoneTimeZoneSuggestion createSuggestion(int phoneId, String zoneId) {
+            PhoneTimeZoneSuggestion suggestion = new PhoneTimeZoneSuggestion(phoneId);
+            suggestion.setZoneId(zoneId);
+            suggestion.setMatchType(matchType);
+            suggestion.setQuality(quality);
+            return suggestion;
+        }
+    }
+
+    private static SuggestionTestCase newTestCase(
+            @MatchType int matchType, @Quality int quality, int expectedScore) {
+        return new SuggestionTestCase(matchType, quality, expectedScore);
+    }
+}