Loading flags/telecom_api_flags.aconfig +12 −0 Original line number Diff line number Diff line Loading @@ -94,3 +94,15 @@ flag { purpose: PURPOSE_FEATURE } } # OWNER=tgunn TARGET=26Q2 flag { name: "call_connected_indicator_preference" is_exported: true namespace: "telecom" description: "Add call connected indicator support for playing a tone or starting a vibration" bug: "146090790" metadata { purpose: PURPOSE_FEATURE } } src/com/android/server/telecom/CallConnectedIndicatorSettings.java 0 → 100644 +75 −0 Original line number Diff line number Diff line /* * 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; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_NONE; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_TONE; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_VIBRATION; import android.content.Context; import android.content.SharedPreferences; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.flags.FeatureFlags; @VisibleForTesting public class CallConnectedIndicatorSettings { private static final String TAG = "CallConnectedIndicatorSettings"; private static final int ALL_SUPPORTED_PREFERENCE = CALL_CONNECTED_INDICATOR_TONE | CALL_CONNECTED_INDICATOR_VIBRATION; private static final String SHARED_PREFERENCES_NAME = "call_connected_indicator_prefs"; private static final String SHARED_PREFERENCES_KEY = "preference_key"; private final Context mContext; private final FeatureFlags mFeatureFlags; private int mCallConnectedIndicator = CALL_CONNECTED_INDICATOR_NONE; public CallConnectedIndicatorSettings(Context context, FeatureFlags flag) { mContext = context; mFeatureFlags = flag; final SharedPreferences prefs = context.getSharedPreferences( SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); mCallConnectedIndicator = prefs.getInt(SHARED_PREFERENCES_KEY, CALL_CONNECTED_INDICATOR_NONE); } public synchronized boolean isCallConnectedVibrationEnabled() { return (mCallConnectedIndicator & CALL_CONNECTED_INDICATOR_VIBRATION) == CALL_CONNECTED_INDICATOR_VIBRATION; } public synchronized boolean isCallConnectedToneEnabled() { return (mCallConnectedIndicator & CALL_CONNECTED_INDICATOR_TONE) == CALL_CONNECTED_INDICATOR_TONE; } public synchronized void setCallConnectedIndicatorPreference(int preference) { if ((preference & ~ALL_SUPPORTED_PREFERENCE) > 0) { throw new IllegalArgumentException("Invalid preference " + preference); } mCallConnectedIndicator = preference; final SharedPreferences prefs = mContext.getSharedPreferences( SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); final SharedPreferences.Editor editor = prefs.edit(); editor.putInt(SHARED_PREFERENCES_KEY, mCallConnectedIndicator); editor.commit(); } public synchronized int getCallConnectedIndicatorPreference() { return mCallConnectedIndicator; } } src/com/android/server/telecom/CallsManager.java +18 −1 Original line number Diff line number Diff line Loading @@ -612,6 +612,8 @@ public class CallsManager extends Call.ListenerBase } }; private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings; /** * Initializes the required Telecom components. */ Loading Loading @@ -751,11 +753,13 @@ public class CallsManager extends Call.ListenerBase mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this); mCallDiagnosticServiceController = callDiagnosticServiceController; mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory); mCallConnectedIndicatorSettings = new CallConnectedIndicatorSettings(context, featureFlags); mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer, ringtoneFactory, vibratorAdapter, new Ringer.VibrationEffectProxy(), mInCallController, mContext.getSystemService(NotificationManager.class), accessibilityManagerAdapter, featureFlags, mAnomalyReporter); accessibilityManagerAdapter, featureFlags, mAnomalyReporter, mCallConnectedIndicatorSettings, asyncTaskExecutor); if (featureFlags.telecomResolveHiddenDependencies()) { // This is now deprecated mCallRecordingTonePlayer = null; Loading Loading @@ -5391,6 +5395,11 @@ public class CallsManager extends Call.ListenerBase stopDtmfTone(call); } // Maybe start vibrating for MO call. if (newState == CallState.ACTIVE && !call.isIncoming() && !call.isUnknown()) { mRinger.startVibratingForOutgoingCallActive(); } // Unfortunately, in the telephony world the radio is king. So if the call notifies // us that the call is in a particular state, we allow it even if it doesn't make // sense (e.g., STATE_ACTIVE -> STATE_RINGING). Loading Loading @@ -7653,4 +7662,12 @@ public class CallsManager extends Call.ListenerBase getPendingAccountSelection() { return mPendingAccountSelection; } public int getCallConnectedIndicatorPreference() { return mCallConnectedIndicatorSettings.getCallConnectedIndicatorPreference(); } public void setCallConnectedIndicatorPreference(int preference) { mCallConnectedIndicatorSettings.setCallConnectedIndicatorPreference(preference); } } src/com/android/server/telecom/Ringer.java +50 −2 Original line number Diff line number Diff line Loading @@ -63,6 +63,7 @@ import java.util.ArrayList; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; Loading Loading @@ -146,6 +147,7 @@ public class Ringer { private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000; private static final int RAMPING_RINGER_DURATION = 10000; private static final int OUTGOING_CALL_VIBRATING_DURATION = 100; static { // construct complete pulse pattern Loading Loading @@ -175,6 +177,16 @@ public class Ringer { 0, // No amplitude while waiting }; private static final long[] CALL_CONNECTED_VIBRATION_PATTERN = { 0, // No delay before starting 1000, // How long to vibrate }; private static final int[] CALL_CONNECTED_VIBRATION_AMPLITUDE = { 0, // No delay before starting 255, // Vibrate full amplitude }; /** * Indicates that vibration should be repeated at element 5 in the {@link #PULSE_AMPLITUDE} and * {@link #PULSE_PATTERN} arrays. This means repetition will happen for the main ease-in/peak Loading Loading @@ -238,7 +250,7 @@ public class Ringer { /** * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls. */ private boolean mIsVibrating = false; private volatile boolean mIsVibrating = false; private Handler mHandler = null; Loading @@ -247,6 +259,11 @@ public class Ringer { * lock */ private final Object mLock; /** * Used to track the status of call connected inidicator preference. */ private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings; private final Executor mAsyncTaskExecutor; /** * Manages a dedicated single background thread for executing Ringer-specific tasks Loading Loading @@ -278,7 +295,9 @@ public class Ringer { NotificationManager notificationManager, AccessibilityManagerAdapter accessibilityManagerAdapter, FeatureFlags featureFlags, AnomalyReporterAdapter anomalyReporter) { AnomalyReporterAdapter anomalyReporter, CallConnectedIndicatorSettings callConnectedIndicator, Executor asyncTaskExecutor) { mLock = new Object(); mSystemSettingsUtil = systemSettingsUtil; Loading Loading @@ -306,6 +325,8 @@ public class Ringer { mFlags = featureFlags; mRingtoneVibrationSupported = mContext.getResources().getBoolean( com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported); mCallConnectedIndicatorSettings = callConnectedIndicator; mAsyncTaskExecutor = asyncTaskExecutor; } public void shutdownExecutor() { Loading Loading @@ -1053,4 +1074,31 @@ public class Ringer { return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN, SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT); } public void startVibratingForOutgoingCallActive() { if (!mFlags.callConnectedIndicatorPreference()) { Log.i(TAG, "Call connected indicator of vibration is disabled."); return; } if (!mIsVibrating && mCallConnectedIndicatorSettings.isCallConnectedVibrationEnabled()) { mIsVibrating = true; mAsyncTaskExecutor.execute(() -> { final VibrationEffect vibrationEffect = mVibrationEffectProxy.createWaveform(CALL_CONNECTED_VIBRATION_PATTERN, CALL_CONNECTED_VIBRATION_AMPLITUDE, -1); final VibrationAttributes vibrationAttributes = new VibrationAttributes.Builder() .setUsage(VibrationAttributes.USAGE_NOTIFICATION) .build(); mVibrator.vibrate(vibrationEffect, vibrationAttributes); try { Thread.sleep(OUTGOING_CALL_VIBRATING_DURATION); } catch (InterruptedException e) { // Womp } mVibrator.cancel(); mIsVibrating = false; }); } } } src/com/android/server/telecom/TelecomServiceImpl.java +47 −0 Original line number Diff line number Diff line Loading @@ -3038,6 +3038,53 @@ public class TelecomServiceImpl { Log.endSession(); } } @Override public int getCallConnectedIndicatorPreference(String callingPackage) { ApiStats.ApiEvent event = new ApiStats.ApiEvent( ApiStats.API_GETCALLCONNECTEDINDICATORPREF, Binder.getCallingUid(), ApiStats.RESULT_PERMISSION); try { Log.startSession("TSI.gCCIPB", Log.getPackageAbbreviation(callingPackage)); enforcePermission(READ_PRIVILEGED_PHONE_STATE); synchronized (mLock) { long token = Binder.clearCallingIdentity(); event.setResult(ApiStats.RESULT_NORMAL); try { return mCallsManager.getCallConnectedIndicatorPreference(); } finally { Binder.restoreCallingIdentity(token); } } } finally { logEvent(event); Log.endSession(); } } @Override public void setCallConnectedIndicatorPreference(String callingPackage, int preference) { ApiStats.ApiEvent event = new ApiStats.ApiEvent( ApiStats.API_SETCALLCONNECTEDINDICATORPREF, Binder.getCallingUid(), ApiStats.RESULT_PERMISSION); try { Log.startSession("TSI.sCCIPB", Log.getPackageAbbreviation(callingPackage)); mContext.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, "MODIFY_PHONE_STATE required."); synchronized (mLock) { long token = Binder.clearCallingIdentity(); event.setResult(ApiStats.RESULT_NORMAL); try { mCallsManager.setCallConnectedIndicatorPreference(preference); } finally { Binder.restoreCallingIdentity(token); } } } finally { logEvent(event); Log.endSession(); } } }; public TelecomServiceImpl( Context context, Loading Loading
flags/telecom_api_flags.aconfig +12 −0 Original line number Diff line number Diff line Loading @@ -94,3 +94,15 @@ flag { purpose: PURPOSE_FEATURE } } # OWNER=tgunn TARGET=26Q2 flag { name: "call_connected_indicator_preference" is_exported: true namespace: "telecom" description: "Add call connected indicator support for playing a tone or starting a vibration" bug: "146090790" metadata { purpose: PURPOSE_FEATURE } }
src/com/android/server/telecom/CallConnectedIndicatorSettings.java 0 → 100644 +75 −0 Original line number Diff line number Diff line /* * 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; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_NONE; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_TONE; import static android.telecom.TelecomManager.CALL_CONNECTED_INDICATOR_VIBRATION; import android.content.Context; import android.content.SharedPreferences; import com.android.internal.annotations.VisibleForTesting; import com.android.server.telecom.flags.FeatureFlags; @VisibleForTesting public class CallConnectedIndicatorSettings { private static final String TAG = "CallConnectedIndicatorSettings"; private static final int ALL_SUPPORTED_PREFERENCE = CALL_CONNECTED_INDICATOR_TONE | CALL_CONNECTED_INDICATOR_VIBRATION; private static final String SHARED_PREFERENCES_NAME = "call_connected_indicator_prefs"; private static final String SHARED_PREFERENCES_KEY = "preference_key"; private final Context mContext; private final FeatureFlags mFeatureFlags; private int mCallConnectedIndicator = CALL_CONNECTED_INDICATOR_NONE; public CallConnectedIndicatorSettings(Context context, FeatureFlags flag) { mContext = context; mFeatureFlags = flag; final SharedPreferences prefs = context.getSharedPreferences( SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); mCallConnectedIndicator = prefs.getInt(SHARED_PREFERENCES_KEY, CALL_CONNECTED_INDICATOR_NONE); } public synchronized boolean isCallConnectedVibrationEnabled() { return (mCallConnectedIndicator & CALL_CONNECTED_INDICATOR_VIBRATION) == CALL_CONNECTED_INDICATOR_VIBRATION; } public synchronized boolean isCallConnectedToneEnabled() { return (mCallConnectedIndicator & CALL_CONNECTED_INDICATOR_TONE) == CALL_CONNECTED_INDICATOR_TONE; } public synchronized void setCallConnectedIndicatorPreference(int preference) { if ((preference & ~ALL_SUPPORTED_PREFERENCE) > 0) { throw new IllegalArgumentException("Invalid preference " + preference); } mCallConnectedIndicator = preference; final SharedPreferences prefs = mContext.getSharedPreferences( SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); final SharedPreferences.Editor editor = prefs.edit(); editor.putInt(SHARED_PREFERENCES_KEY, mCallConnectedIndicator); editor.commit(); } public synchronized int getCallConnectedIndicatorPreference() { return mCallConnectedIndicator; } }
src/com/android/server/telecom/CallsManager.java +18 −1 Original line number Diff line number Diff line Loading @@ -612,6 +612,8 @@ public class CallsManager extends Call.ListenerBase } }; private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings; /** * Initializes the required Telecom components. */ Loading Loading @@ -751,11 +753,13 @@ public class CallsManager extends Call.ListenerBase mCallEndpointController = callEndpointControllerFactory.create(context, mLock, this); mCallDiagnosticServiceController = callDiagnosticServiceController; mCallDiagnosticServiceController.setInCallTonePlayerFactory(playerFactory); mCallConnectedIndicatorSettings = new CallConnectedIndicatorSettings(context, featureFlags); mRinger = new Ringer(playerFactory, context, systemSettingsUtil, asyncRingtonePlayer, ringtoneFactory, vibratorAdapter, new Ringer.VibrationEffectProxy(), mInCallController, mContext.getSystemService(NotificationManager.class), accessibilityManagerAdapter, featureFlags, mAnomalyReporter); accessibilityManagerAdapter, featureFlags, mAnomalyReporter, mCallConnectedIndicatorSettings, asyncTaskExecutor); if (featureFlags.telecomResolveHiddenDependencies()) { // This is now deprecated mCallRecordingTonePlayer = null; Loading Loading @@ -5391,6 +5395,11 @@ public class CallsManager extends Call.ListenerBase stopDtmfTone(call); } // Maybe start vibrating for MO call. if (newState == CallState.ACTIVE && !call.isIncoming() && !call.isUnknown()) { mRinger.startVibratingForOutgoingCallActive(); } // Unfortunately, in the telephony world the radio is king. So if the call notifies // us that the call is in a particular state, we allow it even if it doesn't make // sense (e.g., STATE_ACTIVE -> STATE_RINGING). Loading Loading @@ -7653,4 +7662,12 @@ public class CallsManager extends Call.ListenerBase getPendingAccountSelection() { return mPendingAccountSelection; } public int getCallConnectedIndicatorPreference() { return mCallConnectedIndicatorSettings.getCallConnectedIndicatorPreference(); } public void setCallConnectedIndicatorPreference(int preference) { mCallConnectedIndicatorSettings.setCallConnectedIndicatorPreference(preference); } }
src/com/android/server/telecom/Ringer.java +50 −2 Original line number Diff line number Diff line Loading @@ -63,6 +63,7 @@ import java.util.ArrayList; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; Loading Loading @@ -146,6 +147,7 @@ public class Ringer { private static final int RAMPING_RINGER_VIBRATION_DURATION = 5000; private static final int RAMPING_RINGER_DURATION = 10000; private static final int OUTGOING_CALL_VIBRATING_DURATION = 100; static { // construct complete pulse pattern Loading Loading @@ -175,6 +177,16 @@ public class Ringer { 0, // No amplitude while waiting }; private static final long[] CALL_CONNECTED_VIBRATION_PATTERN = { 0, // No delay before starting 1000, // How long to vibrate }; private static final int[] CALL_CONNECTED_VIBRATION_AMPLITUDE = { 0, // No delay before starting 255, // Vibrate full amplitude }; /** * Indicates that vibration should be repeated at element 5 in the {@link #PULSE_AMPLITUDE} and * {@link #PULSE_PATTERN} arrays. This means repetition will happen for the main ease-in/peak Loading Loading @@ -238,7 +250,7 @@ public class Ringer { /** * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls. */ private boolean mIsVibrating = false; private volatile boolean mIsVibrating = false; private Handler mHandler = null; Loading @@ -247,6 +259,11 @@ public class Ringer { * lock */ private final Object mLock; /** * Used to track the status of call connected inidicator preference. */ private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings; private final Executor mAsyncTaskExecutor; /** * Manages a dedicated single background thread for executing Ringer-specific tasks Loading Loading @@ -278,7 +295,9 @@ public class Ringer { NotificationManager notificationManager, AccessibilityManagerAdapter accessibilityManagerAdapter, FeatureFlags featureFlags, AnomalyReporterAdapter anomalyReporter) { AnomalyReporterAdapter anomalyReporter, CallConnectedIndicatorSettings callConnectedIndicator, Executor asyncTaskExecutor) { mLock = new Object(); mSystemSettingsUtil = systemSettingsUtil; Loading Loading @@ -306,6 +325,8 @@ public class Ringer { mFlags = featureFlags; mRingtoneVibrationSupported = mContext.getResources().getBoolean( com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported); mCallConnectedIndicatorSettings = callConnectedIndicator; mAsyncTaskExecutor = asyncTaskExecutor; } public void shutdownExecutor() { Loading Loading @@ -1053,4 +1074,31 @@ public class Ringer { return vibrationEffectProxy.createWaveform(SIMPLE_VIBRATION_PATTERN, SIMPLE_VIBRATION_AMPLITUDE, REPEAT_SIMPLE_VIBRATION_AT); } public void startVibratingForOutgoingCallActive() { if (!mFlags.callConnectedIndicatorPreference()) { Log.i(TAG, "Call connected indicator of vibration is disabled."); return; } if (!mIsVibrating && mCallConnectedIndicatorSettings.isCallConnectedVibrationEnabled()) { mIsVibrating = true; mAsyncTaskExecutor.execute(() -> { final VibrationEffect vibrationEffect = mVibrationEffectProxy.createWaveform(CALL_CONNECTED_VIBRATION_PATTERN, CALL_CONNECTED_VIBRATION_AMPLITUDE, -1); final VibrationAttributes vibrationAttributes = new VibrationAttributes.Builder() .setUsage(VibrationAttributes.USAGE_NOTIFICATION) .build(); mVibrator.vibrate(vibrationEffect, vibrationAttributes); try { Thread.sleep(OUTGOING_CALL_VIBRATING_DURATION); } catch (InterruptedException e) { // Womp } mVibrator.cancel(); mIsVibrating = false; }); } } }
src/com/android/server/telecom/TelecomServiceImpl.java +47 −0 Original line number Diff line number Diff line Loading @@ -3038,6 +3038,53 @@ public class TelecomServiceImpl { Log.endSession(); } } @Override public int getCallConnectedIndicatorPreference(String callingPackage) { ApiStats.ApiEvent event = new ApiStats.ApiEvent( ApiStats.API_GETCALLCONNECTEDINDICATORPREF, Binder.getCallingUid(), ApiStats.RESULT_PERMISSION); try { Log.startSession("TSI.gCCIPB", Log.getPackageAbbreviation(callingPackage)); enforcePermission(READ_PRIVILEGED_PHONE_STATE); synchronized (mLock) { long token = Binder.clearCallingIdentity(); event.setResult(ApiStats.RESULT_NORMAL); try { return mCallsManager.getCallConnectedIndicatorPreference(); } finally { Binder.restoreCallingIdentity(token); } } } finally { logEvent(event); Log.endSession(); } } @Override public void setCallConnectedIndicatorPreference(String callingPackage, int preference) { ApiStats.ApiEvent event = new ApiStats.ApiEvent( ApiStats.API_SETCALLCONNECTEDINDICATORPREF, Binder.getCallingUid(), ApiStats.RESULT_PERMISSION); try { Log.startSession("TSI.sCCIPB", Log.getPackageAbbreviation(callingPackage)); mContext.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, "MODIFY_PHONE_STATE required."); synchronized (mLock) { long token = Binder.clearCallingIdentity(); event.setResult(ApiStats.RESULT_NORMAL); try { mCallsManager.setCallConnectedIndicatorPreference(preference); } finally { Binder.restoreCallingIdentity(token); } } } finally { logEvent(event); Log.endSession(); } } }; public TelecomServiceImpl( Context context, Loading