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

Commit 9df4a2db authored by Hall Liu's avatar Hall Liu
Browse files

Add API for uploading call composer pictures

Bug: 175435766
Test: atest CallComposerTest
Change-Id: Ieff0e87ca22162aac42f8ebe705ef6167b1edb33
parent 5cab4738
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -41735,6 +41735,8 @@ package android.telephony {
    method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void switchMultiSimConfig(int);
    method public void unregisterPhoneStateListener(@NonNull android.telephony.PhoneStateListener);
    method public void updateAvailableNetworks(@NonNull java.util.List<android.telephony.AvailableNetworkInfo>, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<java.lang.Integer>);
    method public void uploadCallComposerPicture(@NonNull java.nio.file.Path, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.ParcelUuid,android.telephony.TelephonyManager.CallComposerException>);
    method public void uploadCallComposerPicture(@NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.os.ParcelUuid,android.telephony.TelephonyManager.CallComposerException>);
    field public static final String ACTION_CARRIER_MESSAGING_CLIENT_SERVICE = "android.telephony.action.CARRIER_MESSAGING_CLIENT_SERVICE";
    field public static final String ACTION_CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE = "android.telephony.action.CARRIER_SIGNAL_DEFAULT_NETWORK_AVAILABLE";
    field public static final String ACTION_CARRIER_SIGNAL_PCO_VALUE = "android.telephony.action.CARRIER_SIGNAL_PCO_VALUE";
@@ -41877,6 +41879,18 @@ package android.telephony {
    field public static final String VVM_TYPE_OMTP = "vvm_type_omtp";
  }
  public static class TelephonyManager.CallComposerException extends java.lang.Exception {
    ctor public TelephonyManager.CallComposerException(int, @Nullable java.io.IOException);
    method public int getErrorCode();
    method @Nullable public java.io.IOException getIOException();
    field public static final int ERROR_AUTHENTICATION_FAILED = 3; // 0x3
    field public static final int ERROR_FILE_TOO_LARGE = 2; // 0x2
    field public static final int ERROR_INPUT_CLOSED = 4; // 0x4
    field public static final int ERROR_IO_EXCEPTION = 5; // 0x5
    field public static final int ERROR_REMOTE_END_CLOSED = 1; // 0x1
    field public static final int ERROR_UNKNOWN = 0; // 0x0
  }
  public abstract static class TelephonyManager.CellInfoCallback {
    ctor public TelephonyManager.CellInfoCallback();
    method public abstract void onCellInfo(@NonNull java.util.List<android.telephony.CellInfo>);
+1 −0
Original line number Diff line number Diff line
@@ -8503,6 +8503,7 @@ package android.provider {
  }
  public static class CallLog.CallComposerLoggingException extends java.lang.Throwable {
    ctor public CallLog.CallComposerLoggingException(int);
    method public int getErrorCode();
    field public static final int ERROR_INPUT_CLOSED = 3; // 0x3
    field public static final int ERROR_REMOTE_END_CLOSED = 1; // 0x1
+0 −1
Original line number Diff line number Diff line
@@ -148,7 +148,6 @@ public class CallLog {

        private final int mErrorCode;

        /** @hide */
        public CallComposerLoggingException(@CallComposerLoggingError int errorCode) {
            mErrorCode = errorCode;
        }
+346 −3
Original line number Diff line number Diff line
@@ -56,8 +56,10 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.ParcelFileDescriptor;
import android.os.ParcelUuid;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Process;
@@ -118,8 +120,13 @@ import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.SmsApplication;
import com.android.telephony.Rlog;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -553,12 +560,12 @@ public class TelephonyManager {
    private static final int MAXIMUM_CALL_COMPOSER_PICTURE_SIZE = 80000;
    // TODO(hallliu): link to upload method in docs
    /**
     * Indicates the maximum size of the call composure picture.
     *
     * Pictures sent via uploadCallComposerPicture must not exceed this size, or an
     * {@link IllegalArgumentException} will be thrown.
     * Pictures sent via {@link #uploadCallComposerPicture(InputStream, Executor, OutcomeReceiver)}
     * or {@link #uploadCallComposerPicture(Path, Executor, OutcomeReceiver)} must not exceed this
     * size, or an error will be returned via the callback in those methods.
     *
     * @return Maximum file size in bytes.
     */
@@ -4241,6 +4248,342 @@ public class TelephonyManager {
        }
    }
    /**
     * Exception that may be supplied to the callback in {@link #uploadCallComposerPicture} if
     * something goes awry.
     */
    public static class CallComposerException extends Exception {
        /**
         * Used internally only, signals success of the upload to the carrier.
         * @hide
         */
        public static final int SUCCESS = -1;
        /**
         * Indicates that an unknown error was encountered when uploading the call composer picture.
         *
         * Clients that encounter this error should retry the upload.
         */
        public static final int ERROR_UNKNOWN = 0;
        /**
         * Indicates that the phone process died or otherwise became unavailable while uploading the
         * call composer picture.
         *
         * Clients that encounter this error should retry the upload.
         */
        public static final int ERROR_REMOTE_END_CLOSED = 1;
        /**
         * Indicates that the file or stream supplied exceeds the size limit defined in
         * {@link #getMaximumCallComposerPictureSize()}.
         *
         * Clients that encounter this error should retry the upload after reducing the size of the
         * picture.
         */
        public static final int ERROR_FILE_TOO_LARGE = 2;
        /**
         * Indicates that the device failed to authenticate with the carrier when uploading the
         * picture.
         *
         * Clients that encounter this error should not retry the upload unless a reboot or radio
         * reset has been performed in the interim.
         */
        public static final int ERROR_AUTHENTICATION_FAILED = 3;
        /**
         * Indicates that the {@link InputStream} passed to {@link #uploadCallComposerPicture}
         * was closed.
         *
         * The caller should retry if this error is encountered, and be sure to not close the stream
         * before the callback is called this time.
         */
        public static final int ERROR_INPUT_CLOSED = 4;
        /**
         * Indicates that an {@link IOException} was encountered while reading the picture.
         *
         * The offending {@link IOException} will be available via {@link #getIOException()}.
         * Clients should use the contents of the exception to determine whether a retry is
         * warranted.
         */
        public static final int ERROR_IO_EXCEPTION = 5;
        /** @hide */
        @IntDef(prefix = {"ERROR_"}, value = {
                ERROR_UNKNOWN,
                ERROR_REMOTE_END_CLOSED,
                ERROR_FILE_TOO_LARGE,
                ERROR_AUTHENTICATION_FAILED,
                ERROR_INPUT_CLOSED,
                ERROR_IO_EXCEPTION,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface CallComposerError {}
        private final int mErrorCode;
        private final IOException mIOException;
        public CallComposerException(@CallComposerError int errorCode,
                @Nullable IOException ioException) {
            mErrorCode = errorCode;
            mIOException = ioException;
        }
        /**
         * Fetches the error code associated with this exception.
         * @return An error code.
         */
        public @CallComposerError int getErrorCode() {
            return mErrorCode;
        }
        /**
         * Fetches the {@link IOException} that caused the error.
         */
        // Follows the naming of IOException
        @SuppressLint("AcronymName")
        public @Nullable IOException getIOException() {
            return mIOException;
        }
    }
    /** @hide */
    public static final String KEY_CALL_COMPOSER_PICTURE_HANDLE = "call_composer_picture_handle";
    /**
     * Uploads a picture to the carrier network for use with call composer.
     *
     * @see #uploadCallComposerPicture(InputStream, Executor, OutcomeReceiver)
     * @param pictureToUpload Path to a local file containing the picture to upload.
     * @param executor The {@link Executor} on which the {@code pictureToUpload} file will be read
     *                 from disk, as well as on which {@code callback} will be called.
     * @param callback A callback called when the upload operation terminates, either in success
     *                 or in error.
     */
    public void uploadCallComposerPicture(@NonNull Path pictureToUpload,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OutcomeReceiver<ParcelUuid, CallComposerException> callback) {
        Objects.requireNonNull(pictureToUpload);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        // Do the role check now so that we can quit early if needed -- there's an additional
        // permission check on the other side of the binder call as well.
        RoleManager rm = mContext.getSystemService(RoleManager.class);
        if (!rm.isRoleHeld(RoleManager.ROLE_DIALER)) {
            throw new SecurityException("You must hold RoleManager.ROLE_DIALER to do this");
        }
        executor.execute(() -> {
            try {
                if (Looper.getMainLooper().isCurrentThread()) {
                    Log.w(TAG, "Uploading call composer picture on main thread!"
                            + " hic sunt dracones!");
                }
                long size = Files.size(pictureToUpload);
                if (size > getMaximumCallComposerPictureSize()) {
                    callback.onError(new CallComposerException(
                            CallComposerException.ERROR_FILE_TOO_LARGE, null));
                    return;
                }
                InputStream fileStream = Files.newInputStream(pictureToUpload);
                try {
                    uploadCallComposerPicture(fileStream, executor,
                            new OutcomeReceiver<ParcelUuid, CallComposerException>() {
                                @Override
                                public void onResult(ParcelUuid result) {
                                    try {
                                        fileStream.close();
                                    } catch (IOException e) {
                                        // ignore
                                        Log.e(TAG, "Error closing file input stream when"
                                                + " uploading call composer pic");
                                    }
                                    callback.onResult(result);
                                }
                                @Override
                                public void onError(CallComposerException error) {
                                    try {
                                        fileStream.close();
                                    } catch (IOException e) {
                                        // ignore
                                        Log.e(TAG, "Error closing file input stream when"
                                                + " uploading call composer pic");
                                    }
                                    callback.onError(error);
                                }
                            });
                } catch (Exception e) {
                    Log.e(TAG, "Got exception calling into stream-version of"
                            + " uploadCallComposerPicture: " + e);
                    try {
                        fileStream.close();
                    } catch (IOException e1) {
                        // ignore
                        Log.e(TAG, "Error closing file input stream when uploading"
                                + " call composer pic");
                    }
                }
            } catch (IOException e) {
                Log.e(TAG, "IOException when uploading call composer pic:" + e);
                callback.onError(
                        new CallComposerException(CallComposerException.ERROR_IO_EXCEPTION, e));
            }
        });
    }
    /**
     * Uploads a picture to the carrier network for use with call composer.
     *
     * This method allows a dialer app to upload a picture to the carrier network that can then
     * later be attached to an outgoing call. In order to attach the picture to a call, use the
     * {@link ParcelUuid} returned from {@code callback} upon successful upload as the value to
     * {@link TelecomManager#EXTRA_OUTGOING_PICTURE}.
     *
     * This functionality is only available to the app filling the {@link RoleManager#ROLE_DIALER}
     * role on the device.
     *
     * @param pictureToUpload An {@link InputStream} that supplies the bytes representing the
     *                        picture to upload. The client bears responsibility for closing this
     *                        stream after {@code callback} is called with success or failure.
     *
     *                        Additionally, if the stream supplies more bytes than the return value
     *                        of {@link #getMaximumCallComposerPictureSize()}, the upload will be
     *                        aborted and the callback will be called with an exception containing
     *                        {@link CallComposerException#ERROR_FILE_TOO_LARGE}.
     * @param executor The {@link Executor} on which the {@code pictureToUpload} stream will be
     *                 read, as well as on which the callback will be called.
     * @param callback A callback called when the upload operation terminates, either in success
     *                 or in error.
     */
    public void uploadCallComposerPicture(@NonNull InputStream pictureToUpload,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OutcomeReceiver<ParcelUuid, CallComposerException> callback) {
        Objects.requireNonNull(pictureToUpload);
        Objects.requireNonNull(executor);
        Objects.requireNonNull(callback);
        ITelephony telephony = getITelephony();
        if (telephony == null) {
            throw new IllegalStateException("Telephony service not available.");
        }
        ParcelFileDescriptor writeFd;
        ParcelFileDescriptor readFd;
        try {
            ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
            writeFd = pipe[1];
            readFd = pipe[0];
        } catch (IOException e) {
            executor.execute(() -> callback.onError(
                    new CallComposerException(CallComposerException.ERROR_IO_EXCEPTION, e)));
            return;
        }
        OutputStream output = new ParcelFileDescriptor.AutoCloseOutputStream(writeFd);
        try {
            telephony.uploadCallComposerPicture(getSubId(), mContext.getOpPackageName(),
                    readFd, new ResultReceiver(null) {
                        @Override
                        protected void onReceiveResult(int resultCode, Bundle result) {
                            if (resultCode != CallComposerException.SUCCESS) {
                                executor.execute(() -> callback.onError(
                                        new CallComposerException(resultCode, null)));
                                return;
                            }
                            ParcelUuid resultUuid =
                                    result.getParcelable(KEY_CALL_COMPOSER_PICTURE_HANDLE);
                            if (resultUuid == null) {
                                Log.e(TAG, "Got null uuid without an error"
                                        + " while uploading call composer pic");
                                executor.execute(() -> callback.onError(
                                        new CallComposerException(
                                                CallComposerException.ERROR_UNKNOWN, null)));
                                return;
                            }
                            executor.execute(() -> callback.onResult(resultUuid));
                        }
                    });
        } catch (RemoteException e) {
            Log.e(TAG, "Remote exception uploading call composer pic:" + e);
            e.rethrowAsRuntimeException();
        }
        executor.execute(() -> {
            if (Looper.getMainLooper().isCurrentThread()) {
                Log.w(TAG, "Uploading call composer picture on main thread!"
                        + " hic sunt dracones!");
            }
            int totalBytesRead = 0;
            byte[] buffer = new byte[16 * 1024];
            try {
                while (true) {
                    int numRead;
                    try {
                        numRead = pictureToUpload.read(buffer);
                    } catch (IOException e) {
                        Log.e(TAG, "IOException reading from input while uploading pic: " + e);
                        // Most likely, this was because the stream was closed. We have no way to
                        // tell though.
                        callback.onError(new CallComposerException(
                                CallComposerException.ERROR_INPUT_CLOSED, e));
                        try {
                            writeFd.closeWithError("input closed");
                        } catch (IOException e1) {
                            // log and ignore
                            Log.e(TAG, "Error closing fd pipe: " + e1);
                        }
                        break;
                    }
                    if (numRead < 0) {
                        break;
                    }
                    totalBytesRead += numRead;
                    if (totalBytesRead > getMaximumCallComposerPictureSize()) {
                        Log.e(TAG, "Read too many bytes from call composer pic stream: "
                                + totalBytesRead);
                        try {
                            callback.onError(new CallComposerException(
                                    CallComposerException.ERROR_FILE_TOO_LARGE, null));
                            writeFd.closeWithError("too large");
                        } catch (IOException e1) {
                            // log and ignore
                            Log.e(TAG, "Error closing fd pipe: " + e1);
                        }
                        break;
                    }
                    try {
                        output.write(buffer, 0, numRead);
                    } catch (IOException e) {
                        callback.onError(new CallComposerException(
                                CallComposerException.ERROR_REMOTE_END_CLOSED, e));
                        try {
                            writeFd.closeWithError("remote end closed");
                        } catch (IOException e1) {
                            // log and ignore
                            Log.e(TAG, "Error closing fd pipe: " + e1);
                        }
                        break;
                    }
                }
            } finally {
                try {
                    output.close();
                } catch (IOException e) {
                    // Ignore -- we might've already closed it.
                }
            }
        });
    }
    /**
     * Returns the Group Identifier Level1 for a GSM phone.
     * Return null if it is unavailable.
+3 −0
Original line number Diff line number Diff line
@@ -1261,6 +1261,9 @@ interface ITelephony {
     */
    int getRadioAccessFamily(in int phoneId, String callingPackage);

    void uploadCallComposerPicture(int subscriptionId, String callingPackage,
            in ParcelFileDescriptor fd, in ResultReceiver callback);

    /**
     * Enables or disables video calling.
     *