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

Commit 1a81a01d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add LocalVoicemailService API." into main

parents 31fed84d 9de07261
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