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

Commit 9de07261 authored by Tyler Gunn's avatar Tyler Gunn
Browse files

Add LocalVoicemailService API.

Adding support for an OEM LocalVoicemailService which can provide on-device
voicemail capabilities.  Telecom ensures there can only be a single call in
local voicemail processing state.

Flag: com.android.server.telecom.flags.local_voicemail
Test: Added new test app for local testing; CTS to come.
API-Coverage-Bug: b/405460896
Bug: 394367444
Bug: 405460896
Change-Id: I671028a3ba6a8f42fc8e326c7ee4a2da553877e6
parent 977ccc3d
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -14942,6 +14942,15 @@ package android.telecom {
    method @Deprecated public void onPhoneDestroyed(android.telecom.Phone);
  }
  @FlaggedApi("com.android.server.telecom.flags.local_voicemail") public abstract class LocalVoicemailService extends android.app.Service {
    ctor public LocalVoicemailService();
    method public final void disconnectCall();
    method @NonNull public java.util.concurrent.Executor getExecutor();
    method @Nullable public android.os.IBinder onBind(@Nullable android.content.Intent);
    method @RequiresPermission(allOf={android.Manifest.permission.CALL_AUDIO_INTERCEPTION, android.Manifest.permission.MODIFY_AUDIO_ROUTING, android.Manifest.permission.RECORD_AUDIO}) public abstract void onVoicemailRequested(@NonNull android.telecom.Call.Details);
    field public static final String SERVICE_INTERFACE = "android.telecom.LocalVoicemailService";
  }
  public class ParcelableCallAnalytics implements android.os.Parcelable {
    ctor public ParcelableCallAnalytics(long, long, int, boolean, boolean, int, int, boolean, String, boolean, java.util.List<android.telecom.ParcelableCallAnalytics.AnalyticsEvent>, java.util.List<android.telecom.ParcelableCallAnalytics.EventTiming>);
    ctor public ParcelableCallAnalytics(android.os.Parcel);
+196 −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 android.telecom;

import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.RemoteException;

import com.android.internal.telecom.ILocalVoicemailService;
import com.android.internal.telecom.ILocalVoicemailServiceAdapter;
import com.android.server.telecom.flags.Flags;

import java.util.concurrent.Executor;

/**
 * A pre-loaded application can provide an implementation of {@link LocalVoicemailService} in order
 * to provide on-device voicemail capabilities.
 * <p>
 * Local voicemail is triggered when:
 * <ul>
 *      <ol>When a call has been in a {@link android.telecom.Call#STATE_RINGING} state and the user
 *      does not answer it within a period of time.</ol>
 *      <ol>When the user rejects the call via {@link android.telecom.Call#reject(int)}.</ol>
 * </ul>
 * <p>
 * When local voicemail is triggered, Telecom calls
 * {@link #onVoicemailRequested(AudioTrack, AudioRecord, Call.Details)} so that the voicemail app
 * can perform voicemail operations.
 * <p>
 * The voicemail app can call {@link #disconnectCall()} to terminate the call.  The voicemail
 * service may want to do this to limit the length of incoming messages.
 *
 * @hide
 */
@SystemApi
@FlaggedApi(Flags.FLAG_LOCAL_VOICEMAIL)
public abstract class LocalVoicemailService extends Service {
    /**
     * The {@link Intent} that must be declared as handled by the service.
     */
    @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
    public static final String SERVICE_INTERFACE = "android.telecom.LocalVoicemailService";

    private ILocalVoicemailServiceAdapter mAdapter;
    private String mCallId;
    private Executor mExecutor;

    /**
     * Handles incoming requests from Telecom to the {@link LocalVoicemailService}.
     */
    private final class LocalVoicemailServiceBinder extends ILocalVoicemailService.Stub {
        @Override
        public void setAdapter(ILocalVoicemailServiceAdapter adapter) throws RemoteException {
            Log.i(LocalVoicemailService.this, "setAdapter");
            getExecutor().execute(() -> {
                mAdapter = adapter;
            });
        }

        @Override
        public void startLocalVoicemail(ParcelableCall call) throws RemoteException {
            Log.i(LocalVoicemailService.this, "startLocalVoicemail: " + call.getId());
            getExecutor().execute(() -> {
                handleLocalVoicemailRequest(call);
            });
        }
    }

    public LocalVoicemailService() {
    }

    @Override
    public @Nullable IBinder onBind(@Nullable Intent intent) {
        Log.i(this, "onBind");
        return new LocalVoicemailServiceBinder();
    }

    /**
     * Override this method so that your service can provide its own {@link Executor} on which the
     * incoming request from Telecom take place.  If not overridden, a main looper executor is used.
     * @return the {@link Executor} to handle incoming requests on.
     */
    @SuppressLint("OnNameExpected")
    @NonNull public Executor getExecutor() {
        if (mExecutor == null) {
            mExecutor = new HandlerExecutor(Handler.createAsync(getMainLooper()));
        }
        return mExecutor;
    }

    /**
     * Disconnects the current call and stops local voicemail processing.  The
     * {@link LocalVoicemailService} is unbound after this method is called and you will no longer
     * have access to the {@link AudioTrack} and {@link AudioRecord} for the call.
     */
    public final void disconnectCall() {
        try {
            mAdapter.disconnectCall(mCallId);
        } catch (RemoteException e) {
        }
    }

    /**
     * Implement this method to handle a request from Telecom to perform local voicemail for a call.
     * <p>
     * There can ONLY be a single local voicemail session taking place at a time.  Telecom will call
     * this method when either:
     * <ul>
     *     <li>The user did not answer the call before the voicemail timeout.</li>
     *     <li>The user rejected the call in the Dialer app.</li>
     * </ul>
     * <p>
     * Local voicemail usually involves playing a greeting to the caller; this is done by playing
     * the greeting onto the {@code uplinkInjectionTrack}.  Once the greeting is played, you should
     * record the message from the {@code downlinkExtractionTrack}.
     * <p>
     * Note: Your service must have the required permissions or the platform will not bind to it.
     * <p>
     * Use {@link AudioManager#getCallDownlinkExtractionAudioRecord(AudioFormat)} to get an
     * {@link AudioRecord} which you can use to access the incoming call audio.  Use
     * {@link AudioManager#getCallUplinkInjectionAudioTrack(AudioFormat)} to get an
     * {@link AudioTrack} which you can use to send audio out to the call.
     * <p>
     * Below is an example; be aware that {@link AudioManager} places restrictions on the formats
     * which can be used and will throw {@link UnsupportedOperationException} if you provide an
     * invalid format.
     * <pre>
     * {@code
     *     AudioManager audioManager = getApplicationContext().getSystemService(
     *             AudioManager.class);
     *     AudioFormat formatOut = new AudioFormat.Builder().setSampleRate(16000)
     *             .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
     *             .setChannelMask(AudioFormat.CHANNEL_OUT_MONO).build();
     *     AudioTrack uplinkInjectionTrack = audioManager.getCallUplinkInjectionAudioTrack(
     *             formatOut);
     *     AudioFormat formatIn = new AudioFormat.Builder().setSampleRate(16000)
     *             .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
     *             .setChannelMask(AudioFormat.CHANNEL_IN_MONO).build();
     *     AudioRecord downlinkExtractionTrack = audioManager.getCallDownlinkExtractionAudioRecord(
     *             formatIn);
     * }
     * </pre>
     * @param call information about the incoming call including its phone number.
     */
    @RequiresPermission(allOf = {Manifest.permission.CALL_AUDIO_INTERCEPTION,
            Manifest.permission.MODIFY_AUDIO_ROUTING, Manifest.permission.RECORD_AUDIO})
    public abstract void onVoicemailRequested(@NonNull Call.Details call);

    /**
     * Relays a request from Telecom to start local voicemail for a call to the app's
     * {@link #onVoicemailRequested(Call.Details)} implementation.
     * @param call Information about the call.
     */
    private void handleLocalVoicemailRequest(ParcelableCall call) {
        mCallId = call.getId();
        try {
            AudioManager audioManager = getApplicationContext().getSystemService(
                    AudioManager.class);
            Log.i(this,
                    "isPstnCallAudioInterceptable: " + audioManager.isPstnCallAudioInterceptable());

            onVoicemailRequested(Call.Details.createFromParcelableCall(call));
        } catch (Exception e) {
            Log.e(this, e, "handleLocalVoicemailRequest: " + call.getId());
        }
    }
}
+30 −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.internal.telecom;

import android.telecom.ParcelableCall;
import com.android.internal.telecom.ILocalVoicemailServiceAdapter;

/**
 * Internal remote interface for a call diagnostic service.
 * @see android.telecom.LocalVoicemailService
 * @hide
 */
oneway interface ILocalVoicemailService {
    void setAdapter(in ILocalVoicemailServiceAdapter adapter);
    void startLocalVoicemail(in ParcelableCall call);
}
 No newline at end of file
+26 −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.internal.telecom;

/**
 * Internal return interface for a local voicemail service.
 * @see android.telecom.LocalVoicemailService
 * @hide
 */
oneway interface ILocalVoicemailServiceAdapter {
    void disconnectCall(in String callId);
}
 No newline at end of file