Merge "fix NPE in CSW#oCC when the ServiceInterface is cleared out" into main
diff --git a/flags/telecom_call_flags.aconfig b/flags/telecom_call_flags.aconfig
index 2fde55a..2c53938 100644
--- a/flags/telecom_call_flags.aconfig
+++ b/flags/telecom_call_flags.aconfig
@@ -3,6 +3,13 @@
 
 # OWNER=tjstuart TARGET=24Q3
 flag {
+  name: "prevent_redundant_location_permission_grant_and_revoke"
+  namespace: "telecom"
+  description: "avoid redundant action of grant and revoke location permission for multiple emergency calls"
+  bug: "345386002"
+}
+
+flag {
   name: "transactional_cs_verifier"
   namespace: "telecom"
   description: "verify connection service callbacks via a transaction"
@@ -16,6 +23,17 @@
   bug: "321369729"
 }
 
+# OWNER=breadley TARGET=24Q4
+flag {
+  name: "cache_call_events"
+  namespace: "telecom"
+  description: "Cache call events to wait for the ServiceWrapper to be set"
+  bug: "364311190"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
 # OWNER = breadley TARGET=24Q3
 flag {
   name: "cancel_removal_on_emergency_redial"
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 3d69e13..dd04bcf 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -43,8 +43,8 @@
     <string name="respond_via_sms_setting_title_2" msgid="4914853536609553457">"હાજરજવાબમાં ફેરફાર કરો"</string>
     <string name="respond_via_sms_setting_summary" msgid="8054571501085436868"></string>
     <string name="respond_via_sms_edittext_dialog_title" msgid="6579353156073272157">"ઝડપી પ્રતિસાદ"</string>
-    <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર સંદેશ મોકલ્યો."</string>
-    <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g>ને સંદેશ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
+    <string name="respond_via_sms_confirmation_format" msgid="2932395476561267842">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલ્યો."</string>
+    <string name="respond_via_sms_failure_format" msgid="5198680980054596391">"<xliff:g id="PHONE_NUMBER">%s</xliff:g> પર મેસેજ મોકલવામાં નિષ્ફળ રહ્યાં."</string>
     <string name="enable_account_preference_title" msgid="6949224486748457976">"કૉલ કરવા માટેના એકાઉન્ટ"</string>
     <string name="outgoing_call_not_allowed_user_restriction" msgid="3424338207838851646">"ફક્ત કટોકટીના કૉલ્સને મંજૂરી છે."</string>
     <string name="outgoing_call_not_allowed_no_permission" msgid="8590468836581488679">"ફોન પરવાનગી વિના આ ઍપ્લિકેશન આઉટગોઇંગ કૉલ્સ કરી શકતી નથી."</string>
diff --git a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
index 232f00d..fc98991 100644
--- a/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
+++ b/src/com/android/server/telecom/CachedAvailableEndpointsChange.java
@@ -34,6 +34,11 @@
     }
 
     @Override
+    public int getCacheType() {
+        return TYPE_STATE;
+    }
+
+    @Override
     public void executeCallback(CallSourceService service, Call call) {
         service.onAvailableCallEndpointsChanged(call, mAvailableEndpoints);
     }
diff --git a/src/com/android/server/telecom/CachedCallEventQueue.java b/src/com/android/server/telecom/CachedCallEventQueue.java
new file mode 100644
index 0000000..9ce51bf
--- /dev/null
+++ b/src/com/android/server/telecom/CachedCallEventQueue.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.server.telecom;
+
+import android.os.Bundle;
+import android.telecom.Log;
+
+public class CachedCallEventQueue implements CachedCallback {
+    public static final String ID = CachedCallEventQueue.class.getSimpleName();
+
+    private final String mEvent;
+    private final Bundle mExtras;
+
+    public CachedCallEventQueue(String event, Bundle extras) {
+        mEvent = event;
+        mExtras = extras;
+    }
+
+    @Override
+    public int getCacheType() {
+        return TYPE_QUEUE;
+    }
+
+    @Override
+    public void executeCallback(CallSourceService service, Call call) {
+        Log.addEvent(call, LogUtils.Events.CALL_EVENT, mEvent);
+        service.sendCallEvent(call, mEvent, mExtras);
+    }
+
+    @Override
+    public String getCallbackId() {
+        return ID;
+    }
+}
diff --git a/src/com/android/server/telecom/CachedCallback.java b/src/com/android/server/telecom/CachedCallback.java
index 88dad07..c354beb 100644
--- a/src/com/android/server/telecom/CachedCallback.java
+++ b/src/com/android/server/telecom/CachedCallback.java
@@ -22,6 +22,27 @@
  * The callback will be executed once the service is set.
  */
 public interface CachedCallback {
+
+    /**
+     * This callback is caching a state, meaning any new CachedCallbacks with the same
+     * {@link #getCallbackId()} will REPLACE any existing CachedCallback.
+     */
+    int TYPE_STATE = 0;
+    /**
+     * This callback is caching a Queue, meaning that any new CachedCallbacks with the same
+     * {@link #getCallbackId()} will enqueue as a FIFO queue and each instance of this
+     * CachedCallback will run {@link #executeCallback(CallSourceService, Call)}.
+     */
+    int TYPE_QUEUE = 1;
+
+    /**
+     * This method allows the callback to determine whether it is caching a {@link #TYPE_STATE} or
+     * a {@link #TYPE_QUEUE}.
+     *
+     * @return Either {@link #TYPE_STATE} or {@link #TYPE_QUEUE} based on the callback type.
+     */
+    int getCacheType();
+
     /**
      * This method executes the callback that was cached because the service was not available
      * at the time the callback was ready.
@@ -33,11 +54,19 @@
     void executeCallback(CallSourceService service, Call call);
 
     /**
-     * This method is helpful for caching the callbacks.  If the callback is called multiple times
-     * while the service is not set, ONLY the last callback should be sent to the client since the
-     * last callback is the most relevant
+     * The ID that this CachedCallback should use to identify itself as a distinct operation.
+     * <p>
+     * If {@link #TYPE_STATE} is set for {@link #getCacheType()}, and a CachedCallback with the
+     * same ID is called multiple times while the service is not set, ONLY the last callback will be
+     * sent to the client since the last callback is the most relevant.
+     * <p>
+     * If {@link #TYPE_QUEUE} is set for {@link #getCacheType()} and the CachedCallback with the
+     * same ID is called multiple times while the service is not set, each CachedCallback will be
+     * enqueued in FIFO order. Once the service is set, {@link #executeCallback} will be called
+     * for each CachedCallback with the same ID.
      *
-     * @return the callback id that is used in a map to only store the last callback value
+     * @return A unique callback id that will be used differentiate this CachedCallback type with
+     * other CachedCallback types.
      */
     String getCallbackId();
 }
diff --git a/src/com/android/server/telecom/CachedCurrentEndpointChange.java b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
index 0d5bac9..1d838f0 100644
--- a/src/com/android/server/telecom/CachedCurrentEndpointChange.java
+++ b/src/com/android/server/telecom/CachedCurrentEndpointChange.java
@@ -33,6 +33,11 @@
     }
 
     @Override
+    public int getCacheType() {
+        return TYPE_STATE;
+    }
+
+    @Override
     public void executeCallback(CallSourceService service, Call call) {
         service.onCallEndpointChanged(call, mCurrentCallEndpoint);
     }
diff --git a/src/com/android/server/telecom/CachedMuteStateChange.java b/src/com/android/server/telecom/CachedMuteStateChange.java
index 45cbfaa..ee1227b 100644
--- a/src/com/android/server/telecom/CachedMuteStateChange.java
+++ b/src/com/android/server/telecom/CachedMuteStateChange.java
@@ -29,6 +29,11 @@
     }
 
     @Override
+    public int getCacheType() {
+        return TYPE_STATE;
+    }
+
+    @Override
     public void executeCallback(CallSourceService service, Call call) {
         service.onMuteStateChanged(call, mIsMuted);
     }
diff --git a/src/com/android/server/telecom/CachedVideoStateChange.java b/src/com/android/server/telecom/CachedVideoStateChange.java
index 0892c33..cefb92b 100644
--- a/src/com/android/server/telecom/CachedVideoStateChange.java
+++ b/src/com/android/server/telecom/CachedVideoStateChange.java
@@ -33,6 +33,11 @@
     }
 
     @Override
+    public int getCacheType() {
+        return TYPE_STATE;
+    }
+
+    @Override
     public void executeCallback(CallSourceService service, Call call) {
         service.onVideoStateChanged(call, mCurrentVideoState);
         Log.addEvent(call, LogUtils.Events.VIDEO_STATE_CHANGED,
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index ccf2ae3..ba371f1 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -19,6 +19,8 @@
 import static android.provider.CallLog.Calls.MISSED_REASON_NOT_MISSED;
 import static android.telephony.TelephonyManager.EVENT_DISPLAY_EMERGENCY_MESSAGE;
 
+import static com.android.server.telecom.CachedCallback.TYPE_QUEUE;
+import static com.android.server.telecom.CachedCallback.TYPE_STATE;
 import static com.android.server.telecom.voip.VideoStateTranslation.TransactionalVideoStateToString;
 import static com.android.server.telecom.voip.VideoStateTranslation.VideoProfileStateToTransactionalVideoState;
 
@@ -849,14 +851,51 @@
      */
     private CompletableFuture<Boolean> mBtIcsFuture;
 
-    Map<String, CachedCallback> mCachedServiceCallbacks = new HashMap<>();
+    /**
+     * Map of CachedCallbacks that are pending to be executed when the *ServiceWrapper connects
+     */
+    private final Map<String, List<CachedCallback>> mCachedServiceCallbacks = new HashMap<>();
 
     public void cacheServiceCallback(CachedCallback callback) {
-        mCachedServiceCallbacks.put(callback.getCallbackId(), callback);
+        synchronized (mCachedServiceCallbacks) {
+            if (mFlags.cacheCallEvents()) {
+                // If there are multiple threads caching + calling processCachedCallbacks at the
+                // same time, there is a race - double check here to ensure that we do not lose an
+                // operation due to a a cache happening after processCachedCallbacks.
+                // Either service will be non-null in this case, but both will not be non-null
+                if (mConnectionService != null) {
+                    callback.executeCallback(mConnectionService, this);
+                    return;
+                }
+                if (mTransactionalService != null) {
+                    callback.executeCallback(mTransactionalService, this);
+                    return;
+                }
+            }
+            List<CachedCallback> cbs = mCachedServiceCallbacks.computeIfAbsent(
+                    callback.getCallbackId(), k -> new ArrayList<>());
+            switch (callback.getCacheType()) {
+                case TYPE_STATE: {
+                    cbs.clear();
+                    cbs.add(callback);
+                    break;
+                }
+                case TYPE_QUEUE: {
+                    cbs.add(callback);
+                }
+            }
+        }
     }
 
-    public Map<String, CachedCallback> getCachedServiceCallbacks() {
-        return mCachedServiceCallbacks;
+    @VisibleForTesting
+    public Map<String, List<CachedCallback>> getCachedServiceCallbacksCopy() {
+        synchronized (mCachedServiceCallbacks) {
+            // This should only be used during testing, but to be safe, since there is internally a
+            // List value, we need to do a deep copy to ensure someone with a ref to the Map doesn't
+            // mutate the underlying list while we are modifying it in cacheServiceCallback.
+            return mCachedServiceCallbacks.entrySet().stream().collect(
+                    Collectors.toUnmodifiableMap(Map.Entry::getKey, e-> List.copyOf(e.getValue())));
+        }
     }
 
     private FeatureFlags mFlags;
@@ -2052,11 +2091,13 @@
 
     private void processCachedCallbacks(CallSourceService service) {
         if(mFlags.cacheCallAudioCallbacks()) {
-            for (CachedCallback callback : mCachedServiceCallbacks.values()) {
-                callback.executeCallback(service, this);
+            synchronized (mCachedServiceCallbacks) {
+                for (List<CachedCallback> callbacks : mCachedServiceCallbacks.values()) {
+                    callbacks.forEach( callback -> callback.executeCallback(service, this));
+                }
+                // clear list for memory cleanup purposes. The Service should never be reset
+                mCachedServiceCallbacks.clear();
             }
-            // clear list for memory cleanup purposes. The Service should never be reset
-            mCachedServiceCallbacks.clear();
         }
     }
 
@@ -3520,26 +3561,12 @@
     }
 
     /**
-     * Sends a call event to the {@link ConnectionService} for this call. This function is
-     * called for event other than {@link Call#EVENT_REQUEST_HANDOVER}
+     * Sends a call event to the {@link ConnectionService} for this call.
      *
      * @param event The call event.
      * @param extras Associated extras.
      */
     public void sendCallEvent(String event, Bundle extras) {
-        sendCallEvent(event, 0/*For Event != EVENT_REQUEST_HANDOVER*/, extras);
-    }
-
-    /**
-     * Sends a call event to the {@link ConnectionService} for this call.
-     *
-     * See {@link Call#sendCallEvent(String, Bundle)}.
-     *
-     * @param event The call event.
-     * @param targetSdkVer SDK version of the app calling this api
-     * @param extras Associated extras.
-     */
-    public void sendCallEvent(String event, int targetSdkVer, Bundle extras) {
         if (mConnectionService != null || mTransactionalService != null) {
             // Relay bluetooth call quality reports to the call diagnostic service.
             if (BluetoothCallQualityReport.EVENT_BLUETOOTH_CALL_QUALITY_REPORT.equals(event)
@@ -3552,19 +3579,25 @@
             Log.addEvent(this, LogUtils.Events.CALL_EVENT, event);
             sendEventToService(this, event, extras);
         } else {
-            Log.e(this, new NullPointerException(),
-                    "sendCallEvent failed due to null CS callId=%s", getId());
+            if (mFlags.cacheCallEvents()) {
+                Log.i(this, "sendCallEvent: caching call event for callId=%s, event=%s",
+                        getId(), event);
+                cacheServiceCallback(new CachedCallEventQueue(event, extras));
+            } else {
+                Log.e(this, new NullPointerException(),
+                        "sendCallEvent failed due to null CS callId=%s", getId());
+            }
         }
     }
 
     /**
-     *  This method should only be called from sendCallEvent(String, int, Bundle).
+     *  This method should only be called from sendCallEvent(String, Bundle).
      */
     private void sendEventToService(Call call, String event, Bundle extras) {
         if (mConnectionService != null) {
             mConnectionService.sendCallEvent(call, event, extras);
         } else if (mTransactionalService != null) {
-            mTransactionalService.onEvent(call, event, extras);
+            mTransactionalService.sendCallEvent(call, event, extras);
         }
     }
 
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 9446746..abee2a8 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -1142,7 +1142,8 @@
                     : mSpeakerDockRoute;
             // Ensure that we default to speaker route if we're in a video call, but disregard it if
             // a wired headset is plugged in.
-            if (skipEarpiece && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
+            if (skipEarpiece && defaultRoute != null
+                    && defaultRoute.getType() == AudioRoute.TYPE_EARPIECE) {
                 Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
                         + "speaker route for video call.");
                 defaultRoute = mSpeakerDockRoute;
diff --git a/src/com/android/server/telecom/CallSourceService.java b/src/com/android/server/telecom/CallSourceService.java
index d579542..6f16129 100644
--- a/src/com/android/server/telecom/CallSourceService.java
+++ b/src/com/android/server/telecom/CallSourceService.java
@@ -16,6 +16,7 @@
 
 package com.android.server.telecom;
 
+import android.os.Bundle;
 import android.telecom.CallEndpoint;
 
 import java.util.Set;
@@ -37,4 +38,6 @@
     void onAvailableCallEndpointsChanged(Call activeCall, Set<CallEndpoint> availableCallEndpoints);
 
     void onVideoStateChanged(Call activeCall, int videoState);
+
+    void sendCallEvent(Call activeCall, String event, Bundle extras);
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 328b8ac..44686b7 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -2304,7 +2304,8 @@
         }
     }
 
-    void sendCallEvent(Call call, String event, Bundle extras) {
+    @Override
+    public void sendCallEvent(Call call, String event, Bundle extras) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("sendCallEvent")) {
             try {
diff --git a/src/com/android/server/telecom/EmergencyCallHelper.java b/src/com/android/server/telecom/EmergencyCallHelper.java
index 5ab0e99..c0e38ca 100644
--- a/src/com/android/server/telecom/EmergencyCallHelper.java
+++ b/src/com/android/server/telecom/EmergencyCallHelper.java
@@ -24,6 +24,7 @@
 import android.telecom.PhoneAccountHandle;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.flags.FeatureFlags;
 
 /**
  * Helps with emergency calls by:
@@ -51,19 +52,25 @@
     private long mLastEmergencyCallTimestampMillis;
     private long mLastOutgoingEmergencyCallTimestampMillis;
 
+    private final FeatureFlags mFeatureFlags;
+
     @VisibleForTesting
     public EmergencyCallHelper(
             Context context,
             DefaultDialerCache defaultDialerCache,
-            Timeouts.Adapter timeoutsAdapter) {
+            Timeouts.Adapter timeoutsAdapter,
+            FeatureFlags featureFlags) {
         mContext = context;
         mDefaultDialerCache = defaultDialerCache;
         mTimeoutsAdapter = timeoutsAdapter;
+        mFeatureFlags = featureFlags;
     }
 
     @VisibleForTesting
     public void maybeGrantTemporaryLocationPermission(Call call, UserHandle userHandle) {
-        if (shouldGrantTemporaryLocationPermission(call)) {
+        if (shouldGrantTemporaryLocationPermission(call) && (
+                !mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()
+                || !wasGrantedTemporaryLocationPermission())) {
             grantLocationPermission(userHandle);
         }
         if (call != null && call.isEmergencyCall()) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index 514ba48..8836fff 100755
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -606,7 +606,7 @@
                 synchronized (mLock) {
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        call.sendCallEvent(event, targetSdkVer, extras);
+                        call.sendCallEvent(event, extras);
                     } else {
                         Log.w(this, "sendCallEvent, unknown call id: %s", callId);
                     }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 0f62f87..5a6f0ea 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -1301,6 +1301,8 @@
     private ArraySet<String> mAllCarrierPrivilegedApps = new ArraySet<>();
     private ArraySet<String> mActiveCarrierPrivilegedApps = new ArraySet<>();
 
+    private java.lang.Runnable mCallRemovedRunnable;
+
     public InCallController(Context context, TelecomSystem.SyncRoot lock, CallsManager callsManager,
             SystemStateHelper systemStateHelper, DefaultDialerCache defaultDialerCache,
             Timeouts.Adapter timeoutsAdapter, EmergencyCallHelper emergencyCallHelper,
@@ -1516,7 +1518,11 @@
             /** Let's add a 2 second delay before we send unbind to the services to hopefully
              *  give them enough time to process all the pending messages.
              */
-            mHandler.postDelayed(new Runnable("ICC.oCR", mLock) {
+            if (mCallRemovedRunnable != null
+                    && mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()) {
+                mHandler.removeCallbacks(mCallRemovedRunnable);
+            }
+            mCallRemovedRunnable = new Runnable("ICC.oCR", mLock) {
                 @Override
                 public void loggedRun() {
                     // Check again to make sure there are no active calls for the associated user.
@@ -1530,8 +1536,10 @@
                         mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
                     }
                 }
-            }.prepare(), mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
-                    mContext.getContentResolver()));
+            }.prepare();
+            mHandler.postDelayed(mCallRemovedRunnable,
+                    mTimeoutsAdapter.getCallRemoveUnbindInCallServicesDelay(
+                            mContext.getContentResolver()));
         }
         call.removeListener(mCallListener);
         mCallIdMapper.removeCall(call);
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
index 1c26017..c309dd5 100644
--- a/src/com/android/server/telecom/Ringer.java
+++ b/src/com/android/server/telecom/Ringer.java
@@ -31,7 +31,9 @@
 import android.media.AudioAttributes;
 import android.media.AudioManager;
 import android.media.Ringtone;
+import android.media.Utils;
 import android.media.VolumeShaper;
+import android.media.audio.Flags;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -188,6 +190,7 @@
     private final VibrationEffectProxy mVibrationEffectProxy;
     private final boolean mIsHapticPlaybackSupportedByDevice;
     private final FeatureFlags mFlags;
+    private final boolean mRingtoneVibrationSupported;
     /**
      * For unit testing purposes only; when set, {@link #startRinging(Call, boolean)} will complete
      * the future provided by the test using {@link #setBlockOnRingingFuture(CompletableFuture)}.
@@ -259,6 +262,8 @@
 
         mAudioManager = mContext.getSystemService(AudioManager.class);
         mFlags = featureFlags;
+        mRingtoneVibrationSupported = mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported);
     }
 
     @VisibleForTesting
@@ -420,6 +425,9 @@
             if (!isHapticOnly) {
                 ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
                         foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
+            } else if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+                ringtoneInfoSupplier = () -> mRingtoneFactory.getRingtone(
+                        foregroundCall, null, false);
             }
 
             // If vibration will be done, reserve the vibrator.
@@ -471,7 +479,8 @@
                     boolean isUsingAudioCoupledHaptics =
                             !finalHapticChannelsMuted && ringtone != null
                                     && ringtone.hasHapticChannels();
-                    vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect);
+                    vibrateIfNeeded(isUsingAudioCoupledHaptics, foregroundCall, vibrationEffect,
+                            ringtoneUri);
                 } finally {
                     // This is used to signal to tests that the async play() call has completed.
                     if (mBlockOnRingingFuture != null) {
@@ -523,13 +532,20 @@
    }
 
     private void vibrateIfNeeded(boolean isUsingAudioCoupledHaptics, Call foregroundCall,
-            VibrationEffect effect) {
+            VibrationEffect effect, Uri ringtoneUri) {
         if (isUsingAudioCoupledHaptics) {
             Log.addEvent(
                 foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using audio-coupled haptics");
             return;
         }
 
+        if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported
+                && Utils.hasVibration(ringtoneUri)) {
+            Log.addEvent(
+                    foregroundCall, LogUtils.Events.SKIP_VIBRATION, "using custom haptics");
+            return;
+        }
+
         synchronized (mLock) {
             // Ensure the reservation is live. The mIsVibrating check should be redundant.
             if (foregroundCall == mVibratingCall && !mIsVibrating) {
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7ff083f..1cbe846 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -285,7 +285,7 @@
                             mContactsAsyncHelper, mLock);
 
             EmergencyCallHelper emergencyCallHelper = new EmergencyCallHelper(mContext,
-                    defaultDialerCache, timeoutsAdapter);
+                    defaultDialerCache, timeoutsAdapter, mFeatureFlags);
 
             InCallControllerFactory inCallControllerFactory = new InCallControllerFactory() {
                 @Override
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index 50ef2e8..b73de23 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -626,7 +626,8 @@
         }
     }
 
-    public void onEvent(Call call, String event, Bundle extras) {
+    @Override
+    public void sendCallEvent(Call call, String event, Bundle extras) {
         if (call != null) {
             try {
                 mICallEventCallback.onEvent(call.getId(), event, extras);
diff --git a/tests/src/com/android/server/telecom/tests/CallTest.java b/tests/src/com/android/server/telecom/tests/CallTest.java
index 240e641..fa7d21a 100644
--- a/tests/src/com/android/server/telecom/tests/CallTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallTest.java
@@ -23,10 +23,8 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -42,6 +40,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.PersistableBundle;
 import android.os.UserHandle;
 import android.telecom.CallAttributes;
 import android.telecom.CallEndpoint;
@@ -56,12 +55,12 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 import android.telephony.CallQuality;
-import android.widget.Toast;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
 import com.android.server.telecom.CachedAvailableEndpointsChange;
+import com.android.server.telecom.CachedCallEventQueue;
 import com.android.server.telecom.CachedCurrentEndpointChange;
 import com.android.server.telecom.CachedMuteStateChange;
 import com.android.server.telecom.Call;
@@ -216,6 +215,44 @@
     }
 
     @Test
+    public void testMultipleCachedCallEvents() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        when(mFeatureFlags.cacheCallEvents()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+
+        String testEvent1 = "test1";
+        Bundle testBundle1 = new Bundle();
+        testBundle1.putInt("testKey", 1);
+        call.sendCallEvent(testEvent1, testBundle1);
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+        String testEvent2 = "test2";
+        Bundle testBundle2 = new Bundle();
+        testBundle2.putInt("testKey", 2);
+        call.sendCallEvent(testEvent2, testBundle2);
+        assertEquals(2,
+                call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+        String testEvent3 = "test3";
+        Bundle testBundle3 = new Bundle();
+        testBundle2.putInt("testKey", 3);
+        call.sendCallEvent(testEvent3, testBundle3);
+        assertEquals(3,
+                call.getCachedServiceCallbacksCopy().get(CachedCallEventQueue.ID).size());
+
+        verify(tsw, times(0)).sendCallEvent(any(), any(), any());
+        call.setTransactionServiceWrapper(tsw);
+        verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent1), eq(testBundle1));
+        verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent2), eq(testBundle2));
+        verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent3), eq(testBundle3));
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
+    }
+
+    @Test
     public void testMultipleCachedMuteStateChanges() {
         when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
         TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
@@ -224,20 +261,39 @@
         assertNull(call.getTransactionServiceWrapper());
 
         call.cacheServiceCallback(new CachedMuteStateChange(true));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedMuteStateChange.ID).size());
 
         call.cacheServiceCallback(new CachedMuteStateChange(false));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedMuteStateChange.ID).size());
 
         CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedMuteStateChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedMuteStateChange.ID)
+                .getLast();
 
         assertFalse(currentCacheMuteState.isMuted());
 
         call.setTransactionServiceWrapper(tsw);
         verify(tsw, times(1)).onMuteStateChanged(any(), eq(false));
-        assertEquals(0, call.getCachedServiceCallbacks().size());
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
+    }
+
+    @Test
+    public void testCacheAfterServiceSet() {
+        when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        when(mFeatureFlags.cacheCallEvents()).thenReturn(true);
+        TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
+        Call call = createCall("1", Call.CALL_DIRECTION_INCOMING);
+
+        assertNull(call.getTransactionServiceWrapper());
+        call.setTransactionServiceWrapper(tsw);
+        call.cacheServiceCallback(new CachedMuteStateChange(true));
+        // Ensure that we do not lose events if for some reason a CachedCallback is cached after
+        // the service is set
+        verify(tsw, times(1)).onMuteStateChanged(any(), eq(true));
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
     }
 
     @Test
@@ -254,21 +310,24 @@
         assertNull(call.getTransactionServiceWrapper());
 
         call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedCurrentEndpointChange.ID).size());
 
         call.cacheServiceCallback(new CachedCurrentEndpointChange(speaker));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedCurrentEndpointChange.ID).size());
 
         CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedCurrentEndpointChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedCurrentEndpointChange.ID)
+                .getLast();
 
         assertEquals(CallEndpoint.TYPE_SPEAKER,
                 currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
 
         call.setTransactionServiceWrapper(tsw);
         verify(tsw, times(1)).onCallEndpointChanged(any(), any());
-        assertEquals(0, call.getCachedServiceCallbacks().size());
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
     }
 
     @Test
@@ -287,20 +346,23 @@
         assertNull(call.getTransactionServiceWrapper());
 
         call.cacheServiceCallback(new CachedAvailableEndpointsChange(initialSet));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedAvailableEndpointsChange.ID).size());
 
         call.cacheServiceCallback(new CachedAvailableEndpointsChange(finalSet));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1,
+                call.getCachedServiceCallbacksCopy().get(CachedAvailableEndpointsChange.ID).size());
 
         CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedAvailableEndpointsChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedAvailableEndpointsChange.ID)
+                .getLast();
 
         assertEquals(2, availableEndpoints.getAvailableEndpoints().size());
 
         call.setTransactionServiceWrapper(tsw);
         verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
-        assertEquals(0, call.getCachedServiceCallbacks().size());
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
     }
 
     /**
@@ -310,6 +372,7 @@
     @Test
     public void testAllCachedCallbacks() {
         when(mFeatureFlags.cacheCallAudioCallbacks()).thenReturn(true);
+        when(mFeatureFlags.cacheCallEvents()).thenReturn(true);
         TransactionalServiceWrapper tsw = Mockito.mock(TransactionalServiceWrapper.class);
         CallEndpoint earpiece = Mockito.mock(CallEndpoint.class);
         CallEndpoint bluetooth = Mockito.mock(CallEndpoint.class);
@@ -323,23 +386,29 @@
 
         // add cached callbacks
         call.cacheServiceCallback(new CachedMuteStateChange(false));
-        assertEquals(1, call.getCachedServiceCallbacks().size());
+        assertEquals(1, call.getCachedServiceCallbacksCopy().size());
         call.cacheServiceCallback(new CachedCurrentEndpointChange(earpiece));
-        assertEquals(2, call.getCachedServiceCallbacks().size());
+        assertEquals(2, call.getCachedServiceCallbacksCopy().size());
         call.cacheServiceCallback(new CachedAvailableEndpointsChange(availableEndpointsSet));
-        assertEquals(3, call.getCachedServiceCallbacks().size());
+        assertEquals(3, call.getCachedServiceCallbacksCopy().size());
+        String testEvent = "testEvent";
+        Bundle testBundle = new Bundle();
+        call.sendCallEvent("testEvent", testBundle);
 
         // verify the cached callbacks are stored properly within the cache map and the values
         // can be evaluated
         CachedMuteStateChange currentCacheMuteState = (CachedMuteStateChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedMuteStateChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedMuteStateChange.ID)
+                .getLast();
         CachedCurrentEndpointChange currentEndpointChange = (CachedCurrentEndpointChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedCurrentEndpointChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedCurrentEndpointChange.ID)
+                .getLast();
         CachedAvailableEndpointsChange availableEndpoints = (CachedAvailableEndpointsChange) call
-                .getCachedServiceCallbacks()
-                .get(CachedAvailableEndpointsChange.ID);
+                .getCachedServiceCallbacksCopy()
+                .get(CachedAvailableEndpointsChange.ID)
+                .getLast();
         assertFalse(currentCacheMuteState.isMuted());
         assertEquals(CallEndpoint.TYPE_EARPIECE,
                 currentEndpointChange.getCurrentCallEndpoint().getEndpointType());
@@ -352,9 +421,10 @@
         verify(tsw, times(1)).onMuteStateChanged(any(), anyBoolean());
         verify(tsw, times(1)).onCallEndpointChanged(any(), any());
         verify(tsw, times(1)).onAvailableCallEndpointsChanged(any(), any());
+        verify(tsw, times(1)).sendCallEvent(any(), eq(testEvent), eq(testBundle));
 
         // the cache map should be cleared
-        assertEquals(0, call.getCachedServiceCallbacks().size());
+        assertEquals(0, call.getCachedServiceCallbacksCopy().size());
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
index f2ad2f7..cc1c38a 100644
--- a/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
+++ b/tests/src/com/android/server/telecom/tests/EmergencyCallHelperTest.java
@@ -75,7 +75,7 @@
     mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
     when(mContext.getPackageManager()).thenReturn(mPackageManager);
     mEmergencyCallHelper = new EmergencyCallHelper(mContext, mDefaultDialerCache,
-        mTimeoutsAdapter);
+        mTimeoutsAdapter, mFeatureFlags);
     when(mDefaultDialerCache.getSystemDialerApplication()).thenReturn(SYSTEM_DIALER_PACKAGE);
 
     //start with no perms
@@ -185,6 +185,61 @@
 
   @SmallTest
   @Test
+  public void testPermGrantAndRevokeForEmergencyCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
+  public void testPermGrantAndRevokeForMultiEmergencyCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    //first call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    //second call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
+  public void testPermGrantAndRevokeForEmergencyCallAndNormalCall() {
+
+    when(mFeatureFlags.preventRedundantLocationPermissionGrantAndRevoke()).thenReturn(true);
+
+    //first call is emergency call
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    //second call is normal call
+    when(mCall.isEmergencyCall()).thenReturn(false);
+    mEmergencyCallHelper.maybeGrantTemporaryLocationPermission(mCall, mUserHandle);
+    mEmergencyCallHelper.maybeRevokeTemporaryLocationPermission();
+
+    //permissions should be granted then revoked
+    verifyGrantInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyGrantInvokedFor(ACCESS_FINE_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_BACKGROUND_LOCATION);
+    verifyRevokeInvokedFor(ACCESS_FINE_LOCATION);
+  }
+
+  @SmallTest
+  @Test
   public void testNoPermGrantForNonEmergencyCall() {
 
     when(mCall.isEmergencyCall()).thenReturn(false);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 449aa41..bea3fe3 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -230,7 +230,7 @@
                 new ComponentName(SYS_PKG, SYS_CLASS));
         when(mDefaultDialerCache.getBTInCallServicePackages()).thenReturn(new String[] {BT_PKG});
         mEmergencyCallHelper = new EmergencyCallHelper(mMockContext, mDefaultDialerCache,
-                mTimeoutsAdapter);
+                mTimeoutsAdapter, mFeatureFlags);
         when(mMockCallsManager.getRoleManagerAdapter()).thenReturn(mMockRoleManagerAdapter);
         when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
                 .thenReturn(mNotificationManager);
diff --git a/tests/src/com/android/server/telecom/tests/RingerTest.java b/tests/src/com/android/server/telecom/tests/RingerTest.java
index 1510e0c..c4d9678 100644
--- a/tests/src/com/android/server/telecom/tests/RingerTest.java
+++ b/tests/src/com/android/server/telecom/tests/RingerTest.java
@@ -47,6 +47,7 @@
 import android.media.AudioManager;
 import android.media.Ringtone;
 import android.media.VolumeShaper;
+import android.media.audio.Flags;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.UserHandle;
@@ -55,8 +56,10 @@
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
+import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.util.Pair;
@@ -91,7 +94,14 @@
     @Rule
     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
 
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
     private static final Uri FAKE_RINGTONE_URI = Uri.parse("content://media/fake/audio/1729");
+
+    private static final Uri FAKE_VIBRATION_URI = Uri.parse("file://media/fake/vibration/1729");
+
+    private static final String VIBRATION_PARAM = "vibration_uri";
     // Returned when the a URI-based VibrationEffect is attempted, to avoid depending on actual
     // device configuration for ringtone URIs. The actual Uri can be verified via the
     // VibrationEffectProxy mock invocation.
@@ -805,6 +815,37 @@
                 .vibrate(any(VibrationEffect.class), any(VibrationAttributes.class));
     }
 
+    @SmallTest
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_RINGTONE_HAPTICS_CUSTOMIZATION)
+    public void testNoVibrateForSilentRingtoneIfRingtoneHasVibration() throws Exception {
+        Uri FAKE_RINGTONE_VIBRATION_URI =
+                FAKE_RINGTONE_URI.buildUpon().appendQueryParameter(
+                        VIBRATION_PARAM, FAKE_VIBRATION_URI.toString()).build();
+        Ringtone mockRingtone = mock(Ringtone.class);
+        Pair<Uri, Ringtone> ringtoneInfo = new Pair(FAKE_RINGTONE_VIBRATION_URI, mockRingtone);
+        when(mockRingtoneFactory.getRingtone(
+                any(Call.class), nullable(VolumeShaper.Configuration.class), anyBoolean()))
+                .thenReturn(ringtoneInfo);
+        mComponentContextFixture.putBooleanResource(
+                com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported, true);
+        createRingerUnderTest(); // Needed after mock the config.
+
+        mRingerUnderTest.startCallWaiting(mockCall1);
+        when(mockAudioManager.getRingerMode()).thenReturn(AudioManager.RINGER_MODE_VIBRATE);
+        when(mockAudioManager.getStreamVolume(AudioManager.STREAM_RING)).thenReturn(0);
+        enableVibrationWhenRinging();
+        assertFalse(startRingingAndWaitForAsync(mockCall2, false));
+
+        verify(mockRingtoneFactory, atLeastOnce())
+                .getRingtone(any(Call.class), eq(null), eq(false));
+        verifyNoMoreInteractions(mockRingtoneFactory);
+        verify(mockTonePlayer).stopTone();
+        // Skip vibration play in Ringer if a vibration was specified to the ringtone
+        verify(mockVibrator, never()).vibrate(any(VibrationEffect.class),
+                any(VibrationAttributes.class));
+    }
+
     /**
      * Call startRinging and wait for its effects to have played out, to allow reliable assertions
      * after it. The effects are generally "start playing ringtone" and "start vibration" - not