Merge "Track non-telecom calls in telecom metrics." into main
diff --git a/flags/telecom_anomaly_report_flags.aconfig b/flags/telecom_anomaly_report_flags.aconfig
index 5d42b86..bc248c8 100644
--- a/flags/telecom_anomaly_report_flags.aconfig
+++ b/flags/telecom_anomaly_report_flags.aconfig
@@ -27,3 +27,11 @@
purpose: PURPOSE_BUGFIX
}
}
+
+# OWNER=tjstuart TARGET=25Q2
+flag {
+ name: "enable_call_exception_anom_reports"
+ namespace: "telecom"
+ description: "When a new CallException is created, generate an anomaly report for metrics"
+ bug: "308932906"
+}
diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml
index 3b6234d..dbdc258 100644
--- a/res/values-kn/strings.xml
+++ b/res/values-kn/strings.xml
@@ -135,5 +135,5 @@
<string name="call_streaming_notification_action_hang_up" msgid="7017663335289063827">"ಹ್ಯಾಂಗ್ ಅಪ್"</string>
<string name="call_streaming_notification_action_switch_here" msgid="3524180754186221228">"ಇಲ್ಲಿಗೆ ಬದಲಾಯಿಸಿ"</string>
<string name="callFailed_too_many_calls" msgid="4249997210954876420">"ಈಗಾಗಲೇ ಎರಡು ಕರೆಗಳು ಪ್ರಗತಿಯಲ್ಲಿರುವುದರಿಂದ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಾಗುವುದಿಲ್ಲ. ಒಂದು ಕರೆಯನ್ನು ಡಿಸ್ಕನೆಕ್ಟ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಎರಡು ಕರೆಗಳನ್ನು ಒಂದೇ ಕಾನ್ಫರೆನ್ಸ್ನಲ್ಲಿ ವಿಲೀನಗೊಳಿಸಿ."</string>
- <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ಈಗಾಗಲೇ ಪ್ರಗತಿಯಲ್ಲಿರುವ ಕರೆಯನ್ನು ಹೋಲ್ಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲದ ಕಾರಣ ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಯನ್ನು ಡಿಸ್ಕನೆಕ್ಟ್ ಮಾಡಿ."</string>
+ <string name="callFailed_unholdable_call" msgid="7580834131274566524">"ಈಗಾಗಲೇ ಪ್ರಗತಿಯಲ್ಲಿರುವ ಕರೆಯನ್ನು ಹೋಲ್ಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲದ ಕಾರಣ, ಕರೆ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ಹೊಸ ಕರೆಯನ್ನು ಮಾಡುವ ಮೊದಲು ಕರೆಯನ್ನು ಡಿಸ್ಕನೆಕ್ಟ್ ಮಾಡಿ."</string>
</resources>
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 75d8d7d..42d5943 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -3486,7 +3486,8 @@
return Contacts.getLookupUri(mCallerInfo.getContactId(), mCallerInfo.lookupKey);
}
- Uri getRingtone() {
+ @VisibleForTesting
+ public Uri getRingtone() {
return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 854fa3b..5cc868b 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -329,6 +329,10 @@
UUID.fromString("0a86157c-50ca-11ee-be56-0242ac120002");
public static final String TELEPHONY_HAS_DEFAULT_BUT_TELECOM_DOES_NOT_MSG =
"Telephony has a default MO acct but Telecom prompted user for MO";
+ public static final UUID CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID =
+ UUID.fromString("1b6a9b88-5049-4ffa-a52a-134d7c3a40e6");
+ public static final UUID FAILED_TO_SWITCH_FOCUS_ERROR_UUID =
+ UUID.fromString("a1b2c3d4-e5f6-7890-1234-567890abcdef");
public static final int[] OUTGOING_CALL_STATES =
{CallState.CONNECTING, CallState.SELECT_PHONE_ACCOUNT, CallState.DIALING,
@@ -772,7 +776,10 @@
mCallStreamingNotification = callStreamingNotification;
mFeatureFlags = featureFlags;
if (mFeatureFlags.voipCallMonitorRefactor()) {
- mVoipCallMonitor = new VoipCallMonitor(mContext, mLock);
+ mVoipCallMonitor = new VoipCallMonitor(
+ mContext,
+ new Handler(Looper.getMainLooper()),
+ mLock);
mVoipCallMonitorLegacy = null;
} else {
mVoipCallMonitor = null;
@@ -4090,11 +4097,14 @@
if (activeCall.isLocallyDisconnecting()) {
callback.onResult(true);
} else {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: active call could "
- + "not be held or disconnected");
+ String msg = "active call could not be held or disconnected";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCallOld: " + msg);
callback.onError(
- new CallException("activeCall could not be held or disconnected",
+ new CallException(msg,
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
}
}
}
@@ -4110,19 +4120,25 @@
// early
if (!canHold(activeCall) &&
!(supportsHold(activeCall) && areFromSameSource(activeCall, newCall))) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "conditions show the call cannot be held.");
- callback.onError(new CallException("call does not support hold",
+ String msg = "call does not support hold";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+ callback.onError(new CallException(msg,
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
return;
}
// attempt to hold the active call
if (!holdActiveCallForNewCall(newCall)) {
- Log.i(this, "transactionHoldPotentialActiveCallForNewCall: "
- + "attempted to hold call but failed.");
- callback.onError(new CallException("cannot hold active call failed",
+ String msg = "cannot hold active call failed";
+ Log.i(this, "transactionHoldPotentialActiveCallForNewCall: " + msg);
+ callback.onError(new CallException(msg,
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(CANNOT_HOLD_CURRENT_ACTIVE_CALL_ERROR_UUID, msg);
+ }
return;
}
@@ -6835,8 +6851,12 @@
if (mTargetCallFocus.getState() != mPreviousCallState) {
mTargetCallFocus.setState(mPreviousCallState, "resetting call state");
}
- mCallback.onError(new CallException("failed to switch focus to requested call",
+ String msg = "failed to switch focus to requested call";
+ mCallback.onError(new CallException(msg,
CallException.CODE_CALL_CANNOT_BE_SET_TO_ACTIVE));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(FAILED_TO_SWITCH_FOCUS_ERROR_UUID, msg);
+ }
return;
}
// at this point, we know the FocusManager is able to update successfully
diff --git a/src/com/android/server/telecom/RingtoneFactory.java b/src/com/android/server/telecom/RingtoneFactory.java
index c740c24..3e71dc2 100644
--- a/src/com/android/server/telecom/RingtoneFactory.java
+++ b/src/com/android/server/telecom/RingtoneFactory.java
@@ -74,12 +74,18 @@
Ringtone ringtone = null;
if (ringtoneUri != null && userContext != null) {
- // Ringtone URI is explicitly specified. First, try to create a Ringtone with that.
- try {
- ringtone = RingtoneManager.getRingtone(
- userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
- } catch (Exception e) {
- Log.e(this, e, "getRingtone: exception while getting ringtone.");
+ if (currentUserOwnsRingtone(ringtoneUri, incomingCall)) {
+ // Ringtone URI is explicitly specified and owned by the current user - try to
+ // create a Ringtone with that.
+ try {
+ ringtone = RingtoneManager.getRingtone(
+ userContext, ringtoneUri, volumeShaperConfig, audioAttrs);
+ } catch (Exception e) {
+ Log.e(this, e, "getRingtone: exception while getting ringtone.");
+ }
+ } else {
+ Log.w(this, "getRingtone: Failed to verify that the custom ringtone URI"
+ + " is owned by the current user. Falling back to the default ringtone.");
}
}
if (ringtone == null) {
@@ -119,6 +125,21 @@
return new Pair(ringtoneUri, ringtone);
}
+ private static boolean currentUserOwnsRingtone(Uri ringtoneUri, Call incomingCall) {
+ if (TextUtils.isEmpty(ringtoneUri.getUserInfo()) ||
+ incomingCall.getAssociatedUser() == null) {
+ return false;
+ }
+
+ UserHandle associatedUser = incomingCall.getAssociatedUser();
+ if (associatedUser == null) {
+ return false;
+ }
+
+ String currentUserId = String.valueOf(associatedUser.getIdentifier());
+ return currentUserId.equals(ringtoneUri.getUserInfo());
+ }
+
private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
return new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index af094b7..b9841ba 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -27,8 +27,6 @@
import static android.Manifest.permission.READ_SMS;
import static android.Manifest.permission.REGISTER_SIM_SUBSCRIPTION;
import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
-import static android.telecom.CallAttributes.DIRECTION_INCOMING;
-import static android.telecom.CallAttributes.DIRECTION_OUTGOING;
import static android.telecom.CallException.CODE_ERROR_UNKNOWN;
import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS;
@@ -52,8 +50,6 @@
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.Process;
@@ -84,15 +80,12 @@
import com.android.internal.telecom.ICallEventCallback;
import com.android.internal.telecom.ITelecomService;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransactionSequencing;
import com.android.server.telecom.callsequencing.voip.VoipCallMonitor;
import com.android.server.telecom.components.UserCallIntentProcessorFactory;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.metrics.ApiStats;
import com.android.server.telecom.metrics.TelecomMetricsController;
import com.android.server.telecom.settings.BlockedNumbersActivity;
-import com.android.server.telecom.callsequencing.voip.IncomingCallTransaction;
-import com.android.server.telecom.callsequencing.voip.OutgoingCallTransaction;
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
@@ -147,6 +140,13 @@
UUID.fromString("4edf6c8d-1e43-4c94-b0fc-a40c8d80cfe8");
public static final String PLACE_CALL_SECURITY_EXCEPTION_ERROR_MSG =
"Security exception thrown while placing an outgoing call.";
+ public static final UUID CALL_IS_NULL_OR_ID_MISMATCH_UUID =
+ UUID.fromString("b11f3251-474c-4f90-96d6-a256aebc3c19");
+ public static final String CALL_IS_NULL_OR_ID_MISMATCH_MSG =
+ "call is null or id mismatch";
+ public static final UUID ADD_CALL_ON_ERROR_UUID =
+ UUID.fromString("f8e7d6c5-b4a3-9210-8765-432109abcdef");
+
private static final String TAG = "TelecomServiceImpl";
private static final String TIME_LINE_ARG = "timeline";
private static final int DEFAULT_VIDEO_STATE = -1;
@@ -239,6 +239,11 @@
onAddCallControl(callId, callEventCallback, null,
new CallException(ADD_CALL_ERR_MSG,
CODE_ERROR_UNKNOWN));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ CALL_IS_NULL_OR_ID_MISMATCH_UUID,
+ CALL_IS_NULL_OR_ID_MISMATCH_MSG);
+ }
return;
}
@@ -268,6 +273,11 @@
public void onError(@NonNull CallException exception) {
Log.d(TAG, "addCall: onError: e=[%s]", exception.toString());
onAddCallControl(callId, callEventCallback, null, exception);
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ ADD_CALL_ON_ERROR_UUID,
+ exception.getMessage());
+ }
}
});
}
@@ -3024,7 +3034,10 @@
});
mTransactionManager = TransactionManager.getInstance();
- mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags);
+ mTransactionManager.setFeatureFlag(mFeatureFlags);
+ mTransactionManager.setAnomalyReporter(mAnomalyReporter);
+ mTransactionalServiceRepository = new TransactionalServiceRepository(mFeatureFlags,
+ mAnomalyReporter);
mBlockedNumbersManager = mFeatureFlags.telecomMainlineBlockedNumbersManager()
? mContext.getSystemService(BlockedNumbersManager.class)
: null;
diff --git a/src/com/android/server/telecom/TransactionalServiceRepository.java b/src/com/android/server/telecom/TransactionalServiceRepository.java
index 5ae459e..954307a 100644
--- a/src/com/android/server/telecom/TransactionalServiceRepository.java
+++ b/src/com/android/server/telecom/TransactionalServiceRepository.java
@@ -35,9 +35,13 @@
private static final Map<PhoneAccountHandle, TransactionalServiceWrapper> mServiceLookupTable =
new HashMap<>();
private final FeatureFlags mFlags;
+ private final AnomalyReporterAdapter mAnomalyReporter;
- public TransactionalServiceRepository(FeatureFlags flags) {
+ public TransactionalServiceRepository(
+ FeatureFlags flags,
+ AnomalyReporterAdapter anomalyReporter) {
mFlags = flags;
+ mAnomalyReporter = anomalyReporter;
}
public TransactionalServiceWrapper addNewCallForTransactionalServiceWrapper
@@ -50,7 +54,8 @@
Log.d(TAG, "creating a new TSW; handle=[%s]", phoneAccountHandle);
service = new TransactionalServiceWrapper(callEventCallback,
callsManager, phoneAccountHandle, call, this,
- TransactionManager.getInstance(), mFlags.enableCallSequencing());
+ TransactionManager.getInstance(), mFlags.enableCallSequencing(),
+ mFlags, mAnomalyReporter);
} else {
Log.d(TAG, "add a new call to an existing TSW; handle=[%s]", phoneAccountHandle);
service = getTransactionalServiceWrapper(phoneAccountHandle);
diff --git a/src/com/android/server/telecom/TransactionalServiceWrapper.java b/src/com/android/server/telecom/TransactionalServiceWrapper.java
index d63a0bd..cc0d547 100644
--- a/src/com/android/server/telecom/TransactionalServiceWrapper.java
+++ b/src/com/android/server/telecom/TransactionalServiceWrapper.java
@@ -47,9 +47,11 @@
import com.android.server.telecom.callsequencing.TransactionManager;
import com.android.server.telecom.callsequencing.CallTransaction;
import com.android.server.telecom.callsequencing.CallTransactionResult;
+import com.android.server.telecom.flags.FeatureFlags;
import java.util.Locale;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
@@ -92,7 +94,12 @@
private TransactionManager mTransactionManager;
private CallStreamingController mStreamingController;
private final TransactionalCallSequencingAdapter mCallSequencingAdapter;
-
+ private final FeatureFlags mFeatureFlags;
+ private final AnomalyReporterAdapter mAnomalyReporter;
+ public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID =
+ UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb");
+ public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG =
+ "A call update was attempted for a call no longer being tracked";
// Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up
// any calls in the event the application crashes or is force stopped.
@@ -108,7 +115,8 @@
public TransactionalServiceWrapper(ICallEventCallback callEventCallback,
CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call,
TransactionalServiceRepository repo, TransactionManager transactionManager,
- boolean isCallSequencingEnabled) {
+ boolean isCallSequencingEnabled, FeatureFlags featureFlags,
+ AnomalyReporterAdapter anomalyReporterAdapter) {
// passed args
mICallEventCallback = callEventCallback;
mCallsManager = callsManager;
@@ -123,6 +131,8 @@
mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager,
mCallsManager, isCallSequencingEnabled);
setDeathRecipient(callEventCallback);
+ mFeatureFlags = featureFlags;
+ mAnomalyReporter = anomalyReporterAdapter;
}
public TransactionManager getTransactionManager() {
@@ -307,6 +317,11 @@
+ " via TelecomManager#addCall", action, callId),
CODE_CALL_IS_NOT_BEING_TRACKED));
callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle);
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID,
+ CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG);
+ }
}
}
diff --git a/src/com/android/server/telecom/callsequencing/CallSequencingController.java b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
index 67ef358..034f02a 100644
--- a/src/com/android/server/telecom/callsequencing/CallSequencingController.java
+++ b/src/com/android/server/telecom/callsequencing/CallSequencingController.java
@@ -42,7 +42,6 @@
import android.telecom.PhoneAccountHandle;
import android.telephony.AnomalyReporter;
import android.telephony.CarrierConfigManager;
-import android.util.Pair;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.AnomalyReporterAdapter;
@@ -62,6 +61,7 @@
import com.android.server.telecom.stats.CallFailureCause;
import java.util.Objects;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
@@ -81,6 +81,10 @@
private final Context mContext;
private final FeatureFlags mFeatureFlags;
private static String TAG = CallSequencingController.class.getSimpleName();
+ public static final UUID SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID =
+ UUID.fromString("ea094d77-6ea9-4e40-891e-14bff5d485d7");
+ public static final String SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG =
+ "Cannot hold active call";
public CallSequencingController(CallsManager callsManager, Context context,
ClockProxy clockProxy, AnomalyReporterAdapter anomalyReporter,
@@ -217,6 +221,12 @@
callback.onError(
new CallException("activeCall could not be held or disconnected",
CallException.CODE_CANNOT_HOLD_CURRENT_ACTIVE_CALL));
+ if (mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_UUID,
+ SEQUENCING_CANNOT_HOLD_ACTIVE_CALL_MSG
+ );
+ }
}
return CompletableFuture.completedFuture(result);
}, new LoggedHandlerExecutor(mHandler, "CM.mCAA", mCallsManager.getLock()));
diff --git a/src/com/android/server/telecom/callsequencing/TransactionManager.java b/src/com/android/server/telecom/callsequencing/TransactionManager.java
index 2a6431b..98d54da 100644
--- a/src/com/android/server/telecom/callsequencing/TransactionManager.java
+++ b/src/com/android/server/telecom/callsequencing/TransactionManager.java
@@ -25,6 +25,8 @@
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.telecom.AnomalyReporterAdapter;
+import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -32,6 +34,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Queue;
+import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class TransactionManager {
@@ -43,6 +46,12 @@
private final Deque<CallTransaction> mCompletedTransactions;
private CallTransaction mCurrentTransaction;
private boolean mProcessingCallSequencing;
+ private AnomalyReporterAdapter mAnomalyReporter;
+ private FeatureFlags mFeatureFlags;
+ public static final UUID TRANSACTION_MANAGER_TIMEOUT_UUID =
+ UUID.fromString("9ccce52e-6694-4357-9e5e-516a9531b062");
+ public static final String TRANSACTION_MANAGER_TIMEOUT_MSG =
+ "TransactionManager hit a timeout while processing a transaction";
public interface TransactionCompleteListener {
void onTransactionCompleted(CallTransactionResult result, String transactionName);
@@ -67,6 +76,14 @@
return INSTANCE;
}
+ public void setFeatureFlag(FeatureFlags flag){
+ mFeatureFlags = flag;
+ }
+
+ public void setAnomalyReporter(AnomalyReporterAdapter callAnomalyReporter){
+ mAnomalyReporter = callAnomalyReporter;
+ }
+
@VisibleForTesting
public static TransactionManager getTestInstance() {
return new TransactionManager();
@@ -109,6 +126,12 @@
receiver.onError(new CallException(transactionName + " timeout",
CODE_OPERATION_TIMED_OUT));
transactionCompleteFuture.complete(false);
+ if (mFeatureFlags != null && mAnomalyReporter != null &&
+ mFeatureFlags.enableCallExceptionAnomReports()) {
+ mAnomalyReporter.reportAnomaly(
+ TRANSACTION_MANAGER_TIMEOUT_UUID,
+ TRANSACTION_MANAGER_TIMEOUT_MSG);
+ }
} catch (Exception e) {
Log.e(TAG, String.format("onTransactionTimeout: Notifying transaction "
+ " %s resulted in an Exception.", transactionName), e);
diff --git a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
index bd0b932..8c74510 100644
--- a/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
+++ b/src/com/android/server/telecom/callsequencing/voip/VoipCallMonitor.java
@@ -30,9 +30,7 @@
import android.content.Context;
import android.content.ServiceConnection;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.IBinder;
-import android.os.Looper;
import android.os.RemoteException;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
@@ -53,14 +51,16 @@
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
public class VoipCallMonitor extends CallsManagerListenerBase {
- private static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
- private static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+ public static final long NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT = 5000L;
+ public static final long NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT = 5000L;
+ private static final String TAG = VoipCallMonitor.class.getSimpleName();
private static final String DElIMITER = "#";
// This list caches calls that are added to the VoipCallMonitor and need an accompanying
// Call-Style Notification!
- private final List<Call> mNewCallsMissingCallStyleNotification;
+ private final ConcurrentLinkedQueue<Call> mNewCallsMissingCallStyleNotification;
private final ConcurrentHashMap<String, Call> mNotificationIdToCall;
private final ConcurrentHashMap<PhoneAccountHandle, Set<Call>> mAccountHandleToCallMap;
private final ConcurrentHashMap<PhoneAccountHandle, ServiceConnection> mServices;
@@ -70,11 +70,11 @@
private final Context mContext;
private final TelecomSystem.SyncRoot mSyncRoot;
- public VoipCallMonitor(Context context, TelecomSystem.SyncRoot lock) {
+ public VoipCallMonitor(Context context, Handler handler, TelecomSystem.SyncRoot lock) {
mSyncRoot = lock;
mContext = context;
- mHandlerForClass = new Handler(Looper.getMainLooper());
- mNewCallsMissingCallStyleNotification = new ArrayList<>();
+ mHandlerForClass = handler;
+ mNewCallsMissingCallStyleNotification = new ConcurrentLinkedQueue<>();
mNotificationIdToCall = new ConcurrentHashMap<>();
mServices = new ConcurrentHashMap<>();
mAccountHandleToCallMap = new ConcurrentHashMap<>();
@@ -83,21 +83,18 @@
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
if (isCallStyleNotification(sbn)) {
- Log.i(this, "onNotificationPosted: sbn=[%s]", sbn);
- boolean foundCallForNotification = false;
+ Log.i(TAG, "onNotificationPosted: sbn=[%s]", sbn);
// Case 1: Call added to this class (via onCallAdded) BEFORE Call-Style
// Notification is posted by the app (only supported scenario)
- // --> remove the newly added call from
- // mNewCallsMissingCallStyleNotification so FGS is not revoked.
- for (Call call : new ArrayList<>(mNewCallsMissingCallStyleNotification)) {
+ Call newCallNoLongerAwaitingNotification = null;
+ for (Call call : mNewCallsMissingCallStyleNotification) {
if (isNotificationForCall(sbn, call)) {
- Log.i(this, "onNotificationPosted: found a pending "
+ Log.i(TAG, "onNotificationPosted: found a pending "
+ "call=[%s] for sbn.id=[%s]", call, sbn.getId());
mNotificationIdToCall.put(
getNotificationIdToCallKey(sbn),
call);
- removeFromNotificationTracking(call);
- foundCallForNotification = true;
+ newCallNoLongerAwaitingNotification = call;
break;
}
}
@@ -105,11 +102,19 @@
// --> Currently do not support this
// Case 3: Call-Style Notification was updated (ex. incoming -> ongoing)
// --> do nothing
- if (!foundCallForNotification) {
- Log.i(this, "onNotificationPosted: could not find a call for the"
+ if (newCallNoLongerAwaitingNotification == null) {
+ Log.i(TAG, "onNotificationPosted: could not find a call for the"
+ " sbn.id=[%s]. This could mean the notification posted"
+ " BEFORE the call is added (error) or it's an update from"
+ " incoming to ongoing (ok).", sbn.getId());
+ } else {
+ // --> remove the newly added call from
+ // mNewCallsMissingCallStyleNotification so FGS is not revoked when the
+ // timeout is hit in VoipCallMonitor#startMonitoringNotification(...). The
+ // timeout ensures the voip app posts a call-style notification within
+ // 5 seconds!
+ mNewCallsMissingCallStyleNotification
+ .remove(newCallNoLongerAwaitingNotification);
}
}
}
@@ -119,14 +124,17 @@
if (!isCallStyleNotification(sbn)) {
return;
}
- Log.i(this, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn);
+ Log.i(TAG, "onNotificationRemoved: Call-Style notification=[%s] removed", sbn);
Call call = getCallFromStatusBarNotificationId(sbn);
if (call != null) {
- PhoneAccountHandle handle = getTargetPhoneAccount(call);
if (!isCallDisconnected(call)) {
mHandlerForClass.postDelayed(() -> {
if (isCallStillBeingTracked(call)) {
- stopFGSDelegation(call, handle);
+ Log.w(TAG,
+ "onNotificationRemoved: notification has been removed for"
+ + " more than 5 seconds but call still ongoing "
+ + "c=[%s]", call);
+ // TODO:: stopFGSDelegation(call, handle) when b/383403913 is fixed
}
}, NOTIFICATION_REMOVED_BUT_CALL_IS_STILL_ONGOING_TIMEOUT);
}
@@ -185,7 +193,7 @@
new ComponentName(this.getClass().getPackageName(),
this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
} catch (RemoteException e) {
- Log.e(this, e, "Cannot register notification listener");
+ Log.e(TAG, e, "Cannot register notification listener");
}
}
@@ -193,7 +201,7 @@
try {
mNotificationListener.unregisterAsSystemService();
} catch (RemoteException e) {
- Log.e(this, e, "Cannot unregister notification listener");
+ Log.e(TAG, e, "Cannot unregister notification listener");
}
}
@@ -217,11 +225,10 @@
if (!isTransactional(call) || handle == null) {
return;
}
- removeFromNotificationTracking(call);
Set<Call> ongoingCalls = mAccountHandleToCallMap
.computeIfAbsent(handle, k -> new HashSet<>());
ongoingCalls.remove(call);
- Log.d(this, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
+ Log.d(TAG, "onCallRemoved: callList.size=[%d]", ongoingCalls.size());
if (ongoingCalls.isEmpty()) {
stopFGSDelegation(call, handle);
} else {
@@ -230,7 +237,7 @@
}
private void maybeStartFGSDelegation(int pid, int uid, PhoneAccountHandle handle, Call call) {
- Log.i(this, "maybeStartFGSDelegation for call=[%s]", call);
+ Log.i(TAG, "maybeStartFGSDelegation for call=[%s]", call);
if (mActivityManagerInternal != null) {
if (mServices.containsKey(handle)) {
Log.addEvent(call, LogUtils.Events.ALREADY_HAS_FGS_DELEGATION);
@@ -262,34 +269,38 @@
try {
if (mActivityManagerInternal
.startForegroundServiceDelegate(options, fgsConnection)) {
- Log.i(this, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
+ Log.i(TAG, "maybeStartFGSDelegation: startForegroundServiceDelegate success");
} else {
Log.addEvent(call, LogUtils.Events.GAIN_FGS_DELEGATION_FAILED);
}
} catch (Exception e) {
- Log.i(this, "startForegroundServiceDelegate failed due to: " + e);
+ Log.i(TAG, "startForegroundServiceDelegate failed due to: " + e);
}
}
}
@VisibleForTesting
public void stopFGSDelegation(Call call, PhoneAccountHandle handle) {
- Log.i(this, "stopFGSDelegation of call=[%s]", call);
+ Log.i(TAG, "stopFGSDelegation of call=[%s]", call);
if (handle == null) {
return;
}
- // In the event this class is waiting for any new calls to post a notification, remove
- // the call from the notification tracking container!
- Set<Call> ongoingCalls = mAccountHandleToCallMap.get(handle);
- if (ongoingCalls != null) {
- for (Call c : new ArrayList<>(ongoingCalls)) {
- removeFromNotificationTracking(c);
+
+ // In the event this class is waiting for any new calls to post a notification, cleanup
+ List<Call> toRemove = new ArrayList<>();
+ for (Call callAwaitingNotification : mNewCallsMissingCallStyleNotification) {
+ if (handle.equals(callAwaitingNotification.getTargetPhoneAccount())) {
+ Log.d(TAG, "stopFGSDelegation: removing call from notification tracking c=[%s]",
+ callAwaitingNotification);
+ toRemove.add(callAwaitingNotification);
}
}
+ mNewCallsMissingCallStyleNotification.removeAll(toRemove);
+
if (mActivityManagerInternal != null) {
ServiceConnection fgsConnection = mServices.get(handle);
if (fgsConnection != null) {
- Log.i(this, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
+ Log.i(TAG, "stopFGSDelegation: requesting stopForegroundServiceDelegate");
mActivityManagerInternal.stopForegroundServiceDelegate(fgsConnection);
}
}
@@ -301,17 +312,17 @@
String callId = getCallId(call);
// Wait 5 seconds for a CallStyle notification to be posted for the call.
// If the Call-Style Notification is not posted, FGS delegation needs to be revoked!
- Log.i(this, "startMonitoringNotification: starting timeout for call.id=[%s]", callId);
- addToNotificationTracking(call);
+ Log.i(TAG, "startMonitoringNotification: starting timeout for call.id=[%s]", callId);
+ mNewCallsMissingCallStyleNotification.add(call);
// If no notification is posted, stop foreground service delegation!
mHandlerForClass.postDelayed(() -> {
- if (isStillMissingNotification(call)) {
- Log.i(this, "startMonitoringNotification: A Call-Style-Notification"
+ if (mNewCallsMissingCallStyleNotification.contains(call)) {
+ Log.i(TAG, "startMonitoringNotification: A Call-Style-Notification"
+ " for voip-call=[%s] hasn't posted in time,"
+ " stopping delegation for app=[%s].", call, packageName);
stopFGSDelegation(call, handle);
} else {
- Log.i(this, "startMonitoringNotification: found a call-style"
+ Log.i(TAG, "startMonitoringNotification: found a call-style"
+ " notification for call.id[%s] at timeout", callId);
}
}, NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT);
@@ -321,24 +332,6 @@
* Helpers
*/
- private void addToNotificationTracking(Call call) {
- synchronized (mNewCallsMissingCallStyleNotification) {
- mNewCallsMissingCallStyleNotification.add(call);
- }
- }
-
- private boolean isStillMissingNotification(Call call) {
- synchronized (mNewCallsMissingCallStyleNotification) {
- return mNewCallsMissingCallStyleNotification.contains(call);
- }
- }
-
- private void removeFromNotificationTracking(Call call) {
- synchronized (mNewCallsMissingCallStyleNotification) {
- mNewCallsMissingCallStyleNotification.remove(call);
- }
- }
-
private PhoneAccountHandle getTargetPhoneAccount(Call call) {
synchronized (mSyncRoot) {
if (call == null) {
@@ -426,7 +419,7 @@
public boolean hasForegroundServiceDelegation(PhoneAccountHandle handle) {
boolean hasFgs = mServices.containsKey(handle);
- Log.i(this, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
+ Log.i(TAG, "hasForegroundServiceDelegation: handle=[%s], hasFgs=[%b]", handle, hasFgs);
return hasFgs;
}
@@ -434,4 +427,9 @@
public ConcurrentHashMap<PhoneAccountHandle, Set<Call>> getAccountToCallsMapping() {
return mAccountHandleToCallMap;
}
+
+ @VisibleForTesting
+ public ConcurrentLinkedQueue<Call> getNewCallsMissingCallStyleNotificationQueue(){
+ return mNewCallsMissingCallStyleNotification;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java b/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java
new file mode 100644
index 0000000..2a951aa
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/RingtoneFactoryTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 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.tests;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.media.Ringtone;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.RingtoneFactory;
+import com.android.server.telecom.flags.FeatureFlags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+
+@RunWith(JUnit4.class)
+public class RingtoneFactoryTest extends TelecomTestCase {
+ @Mock private Uri mockCustomRingtoneUri;
+ @Mock private CallsManager mockCallsManager;
+ @Mock private FeatureFlags mockFeatureFlags;
+ @Mock Call mockCall;
+ private RingtoneFactory ringtoneFactory;
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = spy(mComponentContextFixture.getTestDouble().getApplicationContext());
+ ringtoneFactory = new RingtoneFactory(mockCallsManager, mContext, mockFeatureFlags);
+ }
+
+ @Override
+ @After
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Test
+ public void testCustomRingtoneAccessibleWhenUserOwnsCustomRingtone() throws Exception {
+ // Current User is User 10:
+ when(mockCall.getAssociatedUser()).thenReturn(new UserHandle(10));
+
+ // Custom ringtone is owned by User 10:
+ when(mockCall.getRingtone()).thenReturn(mockCustomRingtoneUri);
+ when(mockCustomRingtoneUri.getUserInfo()).thenReturn("10");
+
+ // Ensure access to the custom ringtone is allowed:
+ Pair<Uri, Ringtone> ringtonePair = ringtoneFactory.getRingtone(mockCall, null, false);
+ assertEquals(mockCustomRingtoneUri, ringtonePair.first);
+ }
+
+ @SmallTest
+ @Test
+ public void testCustomRingtoneNotAccessibleByOtherUser() throws Exception {
+ // Current User is User 10:
+ when(mockCall.getAssociatedUser()).thenReturn(new UserHandle(0));
+
+ // Custom ringtone is owned by User 10:
+ when(mockCall.getRingtone()).thenReturn(mockCustomRingtoneUri);
+ when(mockCustomRingtoneUri.getUserInfo()).thenReturn("10");
+
+ // Ensure access to the custom ringtone is NOT allowed:
+ Pair<Uri, Ringtone> ringtonePair = ringtoneFactory.getRingtone(mockCall, null, false);
+ assertNotEquals(mockCustomRingtoneUri, ringtonePair.first);
+ }
+}
+
diff --git a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
index fea6135..16b6e44 100644
--- a/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
+++ b/tests/src/com/android/server/telecom/tests/TransactionalServiceWrapperTest.java
@@ -34,6 +34,7 @@
import com.android.internal.telecom.ICallControl;
import com.android.internal.telecom.ICallEventCallback;
+import com.android.server.telecom.AnomalyReporterAdapter;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.TelecomSystem;
@@ -70,6 +71,7 @@
@Mock private TransactionManager mTransactionManager;
@Mock private ICallEventCallback mCallEventCallback;
@Mock private TransactionalServiceRepository mRepository;
+ @Mock private AnomalyReporterAdapter mAnomalyReporterAdapter;
@Mock private IBinder mIBinder;
private final TelecomSystem.SyncRoot mLock = new TelecomSystem.SyncRoot() {};
@@ -84,7 +86,7 @@
Mockito.when(mCallEventCallback.asBinder()).thenReturn(mIBinder);
mTransactionalServiceWrapper = new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
}
@Override
@@ -98,7 +100,7 @@
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
assertEquals(SERVICE_HANDLE, service.getPhoneAccountHandle());
assertEquals(1, service.getNumberOfTrackedCalls());
@@ -109,7 +111,7 @@
TransactionalServiceWrapper service =
new TransactionalServiceWrapper(mCallEventCallback,
mCallsManager, SERVICE_HANDLE, mMockCall1, mRepository, mTransactionManager,
- false /*call sequencing*/);
+ false /*call sequencing*/, mFeatureFlags, mAnomalyReporterAdapter);
assertEquals(1, service.getNumberOfTrackedCalls());
service.trackCall(mMockCall2);
diff --git a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
index 1444b09..1b3856c 100644
--- a/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
+++ b/tests/src/com/android/server/telecom/tests/VoipCallMonitorTest.java
@@ -22,9 +22,13 @@
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
@@ -40,6 +44,7 @@
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
@@ -54,6 +59,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -75,6 +81,7 @@
private static final UserHandle USER_HANDLE_1 = new UserHandle(1);
private static final long TIMEOUT = 6000L;
+ @Mock private Handler mHandler;
@Mock private TelecomSystem.SyncRoot mLock;
@Mock private ActivityManagerInternal mActivityManagerInternal;
@Mock private IBinder mServiceConnection;
@@ -88,7 +95,8 @@
@Before
public void setUp() throws Exception {
super.setUp();
- mMonitor = new VoipCallMonitor(mContext, mLock);
+ mHandler = mock(Handler.class);
+ mMonitor = new VoipCallMonitor(mContext, mHandler, mLock);
mActivityManagerInternal = mock(ActivityManagerInternal.class);
mMonitor.setActivityManagerInternal(mActivityManagerInternal);
mMonitor.registerNotificationListener();
@@ -158,16 +166,18 @@
public void testStartMonitorForOneCall() {
// GIVEN - a single call and notification for a voip app
Call call = createTestCall("testCall", mHandle1User1);
- StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+ StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
// WHEN - the Voip call is added and a notification is posted, verify FGS is gained
addCallAndVerifyFgsIsGained(call);
mMonitor.postNotification(sbn);
+ assertNotificationTimeoutTriggered();
+ assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call));
// THEN - when the Voip call is removed, verify that FGS is revoked for the app
mMonitor.onCallRemoved(call);
mMonitor.removeNotification(sbn);
- verify(mActivityManagerInternal, timeout(TIMEOUT))
+ verify(mActivityManagerInternal, times(1))
.stopForegroundServiceDelegate(any(ServiceConnection.class));
}
@@ -179,25 +189,34 @@
public void testStopDelegation_SameApp() {
// GIVEN - 2 consecutive calls for a single Voip app
Call call1 = createTestCall("testCall1", mHandle1User1);
- StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1);
+ StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
Call call2 = createTestCall("testCall2", mHandle1User1);
- StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1);
+ StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
// WHEN - the second call is added and the first is disconnected
- mMonitor.postNotification(sbn1);
+ // -- add the first all and post the corresponding notification
addCallAndVerifyFgsIsGained(call1);
- mMonitor.postNotification(sbn2);
+ assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+ mMonitor.postNotification(sbn1);
+ assertNotificationTimeoutTriggered();
+ assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+ // -- add the second call and post the corresponding notification
mMonitor.onCallAdded(call2);
- mMonitor.onCallRemoved(call1);
+ assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+ mMonitor.postNotification(sbn2);
+ assertNotificationTimeoutTriggered();
+ assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
// THEN - assert FGS is maintained for the process since there is still an ongoing call
- verify(mActivityManagerInternal, timeout(TIMEOUT).times(0))
- .stopForegroundServiceDelegate(any(ServiceConnection.class));
+ mMonitor.onCallRemoved(call1);
mMonitor.removeNotification(sbn1);
+ assertNotificationTimeoutTriggered();
+ verify(mActivityManagerInternal, times(0))
+ .stopForegroundServiceDelegate(any(ServiceConnection.class));
// once all calls are removed, verify FGS is stopped
mMonitor.onCallRemoved(call2);
mMonitor.removeNotification(sbn2);
- verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ verify(mActivityManagerInternal, times(1))
.stopForegroundServiceDelegate(any(ServiceConnection.class));
}
@@ -245,9 +264,10 @@
*/
@SmallTest
@Test
+ @Ignore("b/383403913") // when b/383403913 is fixed, remove the @Ignore
public void testStopFgsIfCallNotificationIsRemoved_PostedAfterFgsIsGained() {
// GIVEN
- StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1);
+ StatusBarNotification sbn = createStatusBarNotificationFromHandle(mHandle1User1, 1);
// WHEN
// FGS is gained after the call is added to VoipCallMonitor
@@ -259,7 +279,65 @@
// shortly after posting the notification, simulate the user dismissing it
mMonitor.removeNotification(sbn);
// FGS should be removed once the notification is removed
- verify(mActivityManagerInternal, timeout(TIMEOUT)).stopForegroundServiceDelegate(c);
+ assertNotificationTimeoutTriggered();
+ verify(mActivityManagerInternal, times(1)).stopForegroundServiceDelegate(c);
+ }
+
+
+ /**
+ * Tests the behavior of foreground service (FGS) delegation for a VoIP app during a scenario
+ * with two consecutive calls. In this scenario, the first call is disconnected shortly after
+ * being created but the second call continues. The apps foreground service should be
+ * maintained.
+ *
+ * GIVEN: Two calls (call1 and call2) are created for the same VoIP app.
+ * WHEN:
+ * - call1 is added, starting the FGS.
+ * - call2 is added immediately after.
+ * - call1 is removed.
+ * - call1 notification is finally posted (late)
+ * - call1 notification is removed shortly after since the call was disconnected
+ * THEN:
+ * - Verifies that the FGS is NOT stopped while call2 is still active.
+ * - Verifies that the FGS IS stopped after call2 is removed and its notification is gone.
+ */
+ @SmallTest
+ @Test
+ public void test2CallsInQuickSuccession() {
+ // GIVEN - 2 consecutive calls for a single Voip app
+ Call call1 = createTestCall("testCall1", mHandle1User1);
+ StatusBarNotification sbn1 = createStatusBarNotificationFromHandle(mHandle1User1, 1);
+ Call call2 = createTestCall("testCall2", mHandle1User1);
+ StatusBarNotification sbn2 = createStatusBarNotificationFromHandle(mHandle1User1, 2);
+
+ // WHEN - add the calls to the VoipCallMonitor class
+ addCallAndVerifyFgsIsGained(call1);
+ mMonitor.onCallAdded(call2);
+ assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+ assertTrue(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+ // -- mock the app disconnecting the first
+ mMonitor.onCallRemoved(call1);
+ // Shortly after, simulate the notification updates coming in to the class
+ // -- post and remove the first call-style notification
+ mMonitor.postNotification(sbn1);
+ assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call1));
+ mMonitor.removeNotification(sbn1);
+ assertNotificationTimeoutTriggered();
+
+ // -- keep the second notification up since the call will continue
+ mMonitor.postNotification(sbn2);
+ assertFalse(mMonitor.getNewCallsMissingCallStyleNotificationQueue().contains(call2));
+
+ // THEN - assert FGS is maintained for the process since there is still an ongoing call
+ assertNotificationTimeoutTriggered();
+ verify(mActivityManagerInternal, times(0))
+ .stopForegroundServiceDelegate(any(ServiceConnection.class));
+
+ // once all calls are removed, verify FGS is stopped
+ mMonitor.onCallRemoved(call2);
+ mMonitor.removeNotification(sbn2);
+ verify(mActivityManagerInternal, timeout(TIMEOUT).times(1))
+ .stopForegroundServiceDelegate(any(ServiceConnection.class));
}
/**
@@ -291,9 +369,10 @@
.build();
}
- private StatusBarNotification createStatusBarNotificationFromHandle(PhoneAccountHandle handle) {
+ private StatusBarNotification createStatusBarNotificationFromHandle(
+ PhoneAccountHandle handle, int id) {
return new StatusBarNotification(
- handle.getComponentName().getPackageName(), "", 0, "", 0, 0,
+ handle.getComponentName().getPackageName(), "", id, "", 0, 0,
createCallStyleNotification(), handle.getUserHandle(), "", 0);
}
@@ -314,4 +393,17 @@
mServiceConnection);
return serviceConnection;
}
+
+ /**
+ * Verifies that a delayed runnable is posted to the handler to handle the notification timeout.
+ * This also executes the captured runnable to simulate the timeout occurring.
+ */
+ private void assertNotificationTimeoutTriggered() {
+ ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
+ verify(mHandler, atLeastOnce()).postDelayed(
+ runnableCaptor.capture(),
+ eq(VoipCallMonitor.NOTIFICATION_NOT_POSTED_IN_TIME_TIMEOUT));
+ Runnable capturedRunnable = runnableCaptor.getValue();
+ capturedRunnable.run();
+ }
}