【Audio】静音或振动模式下重复来电响铃
一、问题描述
基于 Android 14平台,在静音或振动模式下来电是不会播放铃声的,会存在漏掉来电的情况,需要考虑紧急情况下的重复来电,例如在 10 分钟内来电 4 次,在这种情况下需要响铃来提醒用户。
二、问题分析
packages/services/Telecomm
这里主要分析最接近响铃的流程,主要是在 Telecomm
应用中,最主要的是 Ringer
这个类
public boolean startRinging(Call foregroundCall, boolean isHfpDeviceAttached) {boolean deferBlockOnRingingFuture = false;// try-finally to ensure that the block on ringing future is always called.try {if (foregroundCall == null) {Log.wtf(this, "startRinging called with null foreground call.");return false;}if (foregroundCall.getState() != CallState.RINGING&& foregroundCall.getState() != CallState.SIMULATED_RINGING) {// It's possible for bluetooth to connect JUST as a call goes active, which would// mean the call would start ringing again.Log.i(this, "startRinging called for non-ringing foreground callid=%s",foregroundCall.getId());return false;}// Use completable future to establish a timeout, not intent to make these work outside// the main thread asynchronously// TODO: moving these RingerAttributes calculation out of Telecom lock to avoid blockingCompletableFuture<RingerAttributes> ringerAttributesFuture = CompletableFuture.supplyAsync(() -> getRingerAttributes(foregroundCall, isHfpDeviceAttached),new LoggedHandlerExecutor(getHandler(), "R.sR", null));RingerAttributes attributes = null;try {mAttributesLatch = new CountDownLatch(1);attributes = ringerAttributesFuture.get(RINGER_ATTRIBUTES_TIMEOUT, TimeUnit.MILLISECONDS);} catch (ExecutionException | InterruptedException | TimeoutException e) {// Keep attributes as nullLog.i(this, "getAttributes error: " + e);}if (attributes == null) {Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,"RingerAttributes error");return false;}if (attributes.isEndEarly()) {boolean acquireAudioFocus = attributes.shouldAcquireAudioFocus();if (attributes.letDialerHandleRinging()) {Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Dialer handles");// Dialer will setup a ringtone, provide the audio focus if its audible.acquireAudioFocus |= attributes.isRingerAudible();}if (attributes.isSilentRingingRequested()) {Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING, "Silent ringing "+ "requested");}if (attributes.isWorkProfileInQuietMode()) {Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,"Work profile in quiet mode");}return acquireAudioFocus;}......mUniRinger.updateCVRSRingingState(foregroundCall, attributes.shouldRingForContact());if (foregroundCall.isCVRSCall()) {// used mDefaultVibrationEffect} else if (attributes.isRingerAudible()) {mUniRinger.setVoiceCVRSVolume(true, 0);mUniRinger.muteCVRSFromSilenceRinger(false);mUniRinger.setCVRSPlaying(false);/* @}*/mRingingCall = foregroundCall;Log.addEvent(foregroundCall, LogUtils.Events.START_RINGER);// Because we wait until a contact info query to complete before processing a// call (for the purposes of direct-to-voicemail), the information about custom// ringtones should be available by the time this code executes. We can safely// request the custom ringtone from the call and expect it to be current.if (shouldApplyRampingRinger) {Log.i(this, "create ramping ringer.");float silencePoint = (float) (RAMPING_RINGER_VIBRATION_DURATION)/ (float) (RAMPING_RINGER_VIBRATION_DURATION + RAMPING_RINGER_DURATION);mVolumeShaperConfig =new VolumeShaper.Configuration.Builder().setDuration(RAMPING_RINGER_VIBRATION_DURATION+ RAMPING_RINGER_DURATION).setCurve(new float[]{0.f, silencePoint + EPSILON/*keep monotonicity*/, 1.f},new float[]{0.f, 0.f, 1.f}).setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR).build();if (mSystemSettingsUtil.isAudioCoupledVibrationForRampingRingerEnabled()) {useCustomVibrationEffect = true;}} else {if (DEBUG_RINGER) {Log.i(this, "Create ringer with custom vibration effect");}// Ramping ringtone is not enabled.useCustomVibrationEffect = true;}} else {Log.addEvent(foregroundCall, LogUtils.Events.SKIP_RINGING,"Inaudible: " + attributes.getInaudibleReason()+ " isVibratorEnabled=" + isVibratorEnabled);if (isVibratorEnabled) {// If ringer is not audible for this call, then the phone is in "Vibrate" mode.// Use haptic-only ringtone or do not play anything.isHapticOnly = true;if (DEBUG_RINGER) {Log.i(this, "Set ringtone as haptic only: " + isHapticOnly);}} else {foregroundCall.setUserMissed(USER_MISSED_NO_VIBRATE);return attributes.shouldAcquireAudioFocus(); // ringer not audible}}......// 初始化播放器/* UNISOC: add for CVRS(customized video ringing signal) @{ */if (foregroundCall.isCVRSCall()) {ringtoneSupplier = null;Log.i(this, "is CVRS call, skip ringtone play");/* @}*/}else if (isHapticOnly) {if (hapticChannelsMuted) {Log.i(this,"want haptic only ringtone but haptics are muted, skip ringtone play");ringtoneSupplier = null;} else {ringtoneSupplier = mRingtoneFactory::getHapticOnlyRingtone;}} else {ringtoneSupplier = () -> mRingtoneFactory.getRingtone(foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);}......// 实际播放位置if (ringtoneSupplier != null) {mRingtonePlayer.play(ringtoneSupplier, afterRingtoneLogic);} else {afterRingtoneLogic.accept(/* ringtone= */ null, /* stopped= */ false);}// shouldAcquireAudioFocus is meant to be true, but that check is deferred to here// because until now is when we actually know if the ringtone loading worked.return attributes.shouldAcquireAudioFocus()|| (!isHapticOnly && attributes.isRingerAudible());......
}
获取播放参数,核心变量是 isRingerAudible 用于是否播放铃声
private RingerAttributes getRingerAttributes(Call call, boolean isHfpDeviceAttached) {mAudioManager = mContext.getSystemService(AudioManager.class);RingerAttributes.Builder builder = new RingerAttributes.Builder();LogUtils.EventTimer timer = new EventTimer();boolean isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;timer.record("isVolumeOverZero");boolean shouldRingForContact = shouldRingForContact(call);timer.record("shouldRingForContact");boolean isSelfManaged = call.isSelfManaged();timer.record("isSelfManaged");boolean isSilentRingingRequested = call.isSilentRingingRequested();timer.record("isSilentRingRequested");boolean isRingerAudible = isVolumeOverZero && shouldRingForContact;timer.record("isRingerAudible");String inaudibleReason = "";if (!isRingerAudible) {inaudibleReason = String.format("isVolumeOverZero=%s, shouldRingForContact=%s",isVolumeOverZero, shouldRingForContact);}boolean hasExternalRinger = hasExternalRinger(call);timer.record("hasExternalRinger");// Don't do call waiting operations or vibration unless these are false.boolean isTheaterModeOn = mSystemSettingsUtil.isTheaterModeOn(mContext);timer.record("isTheaterModeOn");boolean letDialerHandleRinging = mInCallController.doesConnectedDialerSupportRinging(call.getAssociatedUser());timer.record("letDialerHandleRinging");boolean isWorkProfileInQuietMode =isProfileInQuietMode(call.getAssociatedUser());timer.record("isWorkProfileInQuietMode");Log.i(this, "startRinging timings: " + timer);boolean endEarly = isTheaterModeOn || letDialerHandleRinging || isSelfManaged ||hasExternalRinger || isSilentRingingRequested || isWorkProfileInQuietMode;if (endEarly) {Log.i(this, "Ending early -- isTheaterModeOn=%s, letDialerHandleRinging=%s, " +"isSelfManaged=%s, hasExternalRinger=%s, silentRingingRequested=%s, " +"isWorkProfileInQuietMode=%s",isTheaterModeOn, letDialerHandleRinging, isSelfManaged, hasExternalRinger,isSilentRingingRequested, isWorkProfileInQuietMode);}// Acquire audio focus under any of the following conditions:// 1. Should ring for contact and there's an HFP device attached// 2. Volume is over zero, we should ring for the contact, and there's a audible ringtone// present. (This check is deferred until ringer knows the ringtone)// 3. The call is self-managed.boolean shouldAcquireAudioFocus = !isWorkProfileInQuietMode &&((isHfpDeviceAttached && shouldRingForContact) || isSelfManaged) ||(call.isCVRSCall() && !endEarly && shouldRingForContact); // UNISOC: add for CVRS(customized video ringing signal)// Set missed reason according to attributesif (!isVolumeOverZero) {call.setUserMissed(USER_MISSED_LOW_RING_VOLUME);}if (!shouldRingForContact) {call.setUserMissed(USER_MISSED_DND_MODE);}mAttributesLatch.countDown();return builder.setEndEarly(endEarly).setLetDialerHandleRinging(letDialerHandleRinging).setAcquireAudioFocus(shouldAcquireAudioFocus).setRingerAudible(isRingerAudible).setInaudibleReason(inaudibleReason).setShouldRingForContact(shouldRingForContact).setSilentRingingRequested(isSilentRingingRequested).setWorkProfileQuietMode(isWorkProfileInQuietMode).build();
}
三、解决方案
修改 Ringer
是核心,通过 统计来电次数来计算是否达到响铃的阈值,是否响铃通过 isRingerAudible
变量控制,通过修改 isRingerAudible 达到响铃的条件,为了不影响未打开 Settings 值的逻辑,使用逻辑与
的短路效应将开关条件前置
packages/services/Telecomm/src/com/android/server/telecom/Ringer.java
diff --git a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/Ringer.java b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/Ringer.java
index 454ac41..b8511bc 100755
--- a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/Ringer.java
+++ b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/Ringer.java
@@ -38,6 +38,7 @@ import android.os.UserManager;import android.os.VibrationAttributes;import android.os.VibrationEffect;import android.os.Vibrator;
+import android.provider.Settings;import android.telecom.Log;import android.telecom.TelecomManager;import android.view.accessibility.AccessibilityManager;
@@ -49,12 +50,16 @@ import com.unisoc.server.telecom.UniTelecomComponentFactory;import java.util.ArrayList;import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutionException;import java.util.concurrent.TimeUnit;import java.util.concurrent.TimeoutException;import java.util.function.BiConsumer;import java.util.function.Supplier;
+import java.util.Iterator;
+import java.util.List;/*** Controls the ringtone player.
@@ -197,6 +202,12 @@ public class Ringer {*/private final Object mLock;+ //*/ support auto mute media and ring in silent.
+ private final ConcurrentHashMap<String, List<Long>> mRecentCallHistory = new ConcurrentHashMap<>();
+ private static final long REPEAT_CALL_WINDOW_MS = 10 * 60 * 1000;
+ private static final int REPEAT_CALL_THRESHOLD = 4;
+ //*/
+public UniRinger mUniRinger;/** Initializes the Ringer. */
@@ -295,7 +306,9 @@ public class Ringer {"RingerAttributes error");return false;}
-
+ //*/ support auto mute media and ring in silent.
+ final boolean forceRing = attributes.isForceRing();
+ //*/if (attributes.isEndEarly()) {boolean acquireAudioFocus = attributes.shouldAcquireAudioFocus();if (attributes.letDialerHandleRinging()) {
@@ -424,7 +437,7 @@ public class Ringer {}} else {ringtoneSupplier = () -> mRingtoneFactory.getRingtone(
- foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted);
+ foregroundCall, mVolumeShaperConfig, finalHapticChannelsMuted, forceRing);}// If vibration will be done, reserve the vibrator.
@@ -706,7 +719,15 @@ public class Ringer {RingerAttributes.Builder builder = new RingerAttributes.Builder();LogUtils.EventTimer timer = new EventTimer();
-
+ //*/ support auto mute media and ring in silent.
+ boolean forceRing = Settings.Secure.getInt(mContext.getContentResolver(),
+ Settings.Secure.RING_IN_SILENT_ENABLED, 0) == 1
+ && mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL
+ && shouldForceRingForRepeatCaller(call);
+ if (forceRing) {
+ Log.i(this, "Force ring is enabled for this call due to repeat calling.");
+ }
+ //*/boolean isVolumeOverZero = mAudioManager.getStreamVolume(AudioManager.STREAM_RING) > 0;timer.record("isVolumeOverZero");boolean shouldRingForContact = shouldRingForContact(call);
@@ -716,7 +737,11 @@ public class Ringer {boolean isSilentRingingRequested = call.isSilentRingingRequested();timer.record("isSilentRingRequested");+ /*/ support auto mute media and ring in silent.boolean isRingerAudible = isVolumeOverZero && shouldRingForContact;
+ /*/
+ boolean isRingerAudible = (isVolumeOverZero && shouldRingForContact) || forceRing;
+ //*/timer.record("isRingerAudible");String inaudibleReason = "";if (!isRingerAudible) {
@@ -774,9 +799,30 @@ public class Ringer {.setShouldRingForContact(shouldRingForContact).setSilentRingingRequested(isSilentRingingRequested).setWorkProfileQuietMode(isWorkProfileInQuietMode)
+ .setForceRing(forceRing).build();}+ //*/ support auto mute media and ring in silent.
+ private boolean shouldForceRingForRepeatCaller(Call call) {
+ if (call == null || call.getHandle() == null) {
+ return false;
+ }
+ final String number = call.getHandle().getSchemeSpecificPart();
+ if (number == null || number.isEmpty()) {
+ return false;
+ }
+ final long currentTime = System.currentTimeMillis();
+ final long expirationTime = currentTime - REPEAT_CALL_WINDOW_MS;
+
+ List<Long> timestamps = mRecentCallHistory.computeIfAbsent(number, k -> new CopyOnWriteArrayList<>());
+ timestamps.removeIf(timestamp -> timestamp < expirationTime);
+ timestamps.add(currentTime);
+ Log.i(this, "Repeat caller check for " + number + ". Call count in last 10 mins: " + timestamps.size());
+ return timestamps.size() >= REPEAT_CALL_THRESHOLD;
+ }
+ //*/
+private boolean isProfileInQuietMode(UserHandle user) {UserManager um = mContext.getSystemService(UserManager.class);return um.isManagedProfile(user.getIdentifier()) && um.isQuietModeEnabled(user);
其中 shouldForceRingForRepeatCaller 方法本来是考虑使用电话数据库查询的方式来实现,但原逻辑中存在超时限制,数据库查询耗时会导致 TimeoutException 异常,并且在 getRingerAttributes 方法中不能添加列表的增减操作会导致 ExecutionException 异常。
RingerAttributes
新增变量 mForceRing
和方法 isForceRing()
来控制是否响铃
diff --git a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingerAttributes.java b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingerAttributes.java
old mode 100644
new mode 100755
index e0d3e1c..e80a573
--- a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingerAttributes.java
+++ b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingerAttributes.java
@@ -26,6 +26,7 @@ public class RingerAttributes {private boolean mShouldRingForContact;private boolean mSilentRingingRequested;private boolean mWorkProfileQuietMode;
+ private boolean mForceRing;public RingerAttributes.Builder setEndEarly(boolean endEarly) {mEndEarly = endEarly;
@@ -67,10 +68,15 @@ public class RingerAttributes {return this;}+ public RingerAttributes.Builder setForceRing(boolean forceRing) {
+ mForceRing = forceRing;
+ return this;
+ }
+public RingerAttributes build() {return new RingerAttributes(mEndEarly, mLetDialerHandleRinging, mAcquireAudioFocus,mRingerAudible, mInaudibleReason, mShouldRingForContact,
- mSilentRingingRequested, mWorkProfileQuietMode);
+ mSilentRingingRequested, mWorkProfileQuietMode, mForceRing);}}@@ -82,11 +88,12 @@ public class RingerAttributes {private boolean mShouldRingForContact;private boolean mSilentRingingRequested;private boolean mWorkProfileQuietMode;
+ private boolean mForceRing;private RingerAttributes(boolean endEarly, boolean letDialerHandleRinging,boolean acquireAudioFocus, boolean ringerAudible, String inaudibleReason,boolean shouldRingForContact, boolean silentRingingRequested,
- boolean workProfileQuietMode) {
+ boolean workProfileQuietMode, boolean forceRing) {mEndEarly = endEarly;mLetDialerHandleRinging = letDialerHandleRinging;mAcquireAudioFocus = acquireAudioFocus;
@@ -95,6 +102,7 @@ public class RingerAttributes {mShouldRingForContact = shouldRingForContact;mSilentRingingRequested = silentRingingRequested;mWorkProfileQuietMode = workProfileQuietMode;
+ mForceRing = forceRing;}public boolean isEndEarly() {
@@ -128,4 +136,8 @@ public class RingerAttributes {public boolean isWorkProfileInQuietMode() {return mWorkProfileQuietMode;}
+
+ public boolean isForceRing() {
+ return mForceRing;
+ }}
使用 AudioAttributes.USAGE_ALARM
闹钟通道播放铃声
diff --git a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingtoneFactory.java b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingtoneFactory.java
index f50d37d..fd96bb8 100755
--- a/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingtoneFactory.java
+++ b/sprdroid14_sys_main/packages/services/Telecomm/src/com/android/server/telecom/RingtoneFactory.java
@@ -72,11 +72,12 @@ public class RingtoneFactory {}public Ringtone getRingtone(Call incomingCall,
- @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted) {
+ @Nullable VolumeShaper.Configuration volumeShaperConfig, boolean hapticChannelsMuted,
+ boolean forceRing) {// Initializing ringtones on the main thread can deadlockThreadUtil.checkNotOnMainThread();- AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(hapticChannelsMuted);
+ AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(hapticChannelsMuted, forceRing);//Unisoc FL1000060354: Support multi-sim ringtone.Context userHandleContext = getContextForUserHandle(incomingCall.getAssociatedUser());
@@ -141,9 +142,10 @@ public class RingtoneFactory {return ringtone;}- private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted) {
+ private AudioAttributes getDefaultRingtoneAudioAttributes(boolean hapticChannelsMuted,
+ boolean forceRing) {return new AudioAttributes.Builder()
- .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setUsage(forceRing ? AudioAttributes.USAGE_ALARM : AudioAttributes.USAGE_NOTIFICATION_RINGTONE).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).setHapticChannelsMuted(hapticChannelsMuted).build();
@@ -157,7 +159,7 @@ public class RingtoneFactory {Uri ringtoneUri = Uri.parse("file://" + mContext.getString(com.android.internal.R.string.config_defaultRingtoneVibrationSound));AudioAttributes audioAttrs = getDefaultRingtoneAudioAttributes(
- /* hapticChannelsMuted */ false);
+ /* hapticChannelsMuted */ false, /* forceRing */ false);Ringtone ringtone = RingtoneManager.getRingtone(mContext, ringtoneUri, /* volumeShaperConfig */ null, audioAttrs);if (ringtone != null) {