Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2e1f3938 authored by Tyler Gunn's avatar Tyler Gunn Committed by Android (Google) Code Review
Browse files

Merge "Implement call connected indicator APIs" into main

parents 16de16f6 4e07559e
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -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
  }
}
+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;
    }
}
+18 −1
Original line number Diff line number Diff line
@@ -612,6 +612,8 @@ public class CallsManager extends Call.ListenerBase
        }
    };

    private final CallConnectedIndicatorSettings mCallConnectedIndicatorSettings;

    /**
     * Initializes the required Telecom components.
     */
@@ -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;
@@ -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).
@@ -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);
    }
}
+50 −2
Original line number Diff line number Diff line
@@ -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;
@@ -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
@@ -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
@@ -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;

@@ -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
@@ -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;
@@ -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() {
@@ -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;
            });
        }
    }
}
+47 −0
Original line number Diff line number Diff line
@@ -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