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

Commit 963b83ce authored by Hall Liu's avatar Hall Liu Committed by Android (Google) Code Review
Browse files

Merge "Add multiuser and shadow support for call composer"

parents 7c6e169d 5cab4738
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -8557,7 +8557,7 @@ package android.printservice.recommendation {
package android.provider {
  public class CallLog {
    method @RequiresPermission(android.Manifest.permission.WRITE_CALL_LOG) public static void storeCallComposerPictureAsUser(@NonNull android.content.Context, @Nullable android.os.UserHandle, @NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.Uri,android.provider.CallLog.CallComposerLoggingException>);
    method @RequiresPermission(allOf={android.Manifest.permission.WRITE_CALL_LOG, android.Manifest.permission.INTERACT_ACROSS_USERS}) public static void storeCallComposerPictureAsUser(@NonNull android.content.Context, @Nullable android.os.UserHandle, @NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.Uri,android.provider.CallLog.CallComposerLoggingException>);
  }
  public static class CallLog.CallComposerLoggingException extends java.lang.Throwable {
+177 −51
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -96,6 +97,10 @@ public class CallLog {
     */
    public static final String SHADOW_AUTHORITY = "call_log_shadow";

    /** @hide */
    public static final Uri SHADOW_CALL_COMPOSER_PICTURE_URI = CALL_COMPOSER_PICTURE_URI.buildUpon()
            .authority(SHADOW_AUTHORITY).build();

    /**
     * Describes an error encountered while storing a call composer picture in the call log.
     * @hide
@@ -154,6 +159,29 @@ public class CallLog {
        public @CallComposerLoggingError int getErrorCode() {
            return mErrorCode;
        }

        @Override
        public String toString() {
            String errorString;
            switch (mErrorCode) {
                case ERROR_UNKNOWN:
                    errorString = "UNKNOWN";
                    break;
                case ERROR_REMOTE_END_CLOSED:
                    errorString = "REMOTE_END_CLOSED";
                    break;
                case ERROR_STORAGE_FULL:
                    errorString = "STORAGE_FULL";
                    break;
                case ERROR_INPUT_CLOSED:
                    errorString = "INPUT_CLOSED";
                    break;
                default:
                    errorString = "[[" + mErrorCode + "]]";
                    break;
            }
            return "CallComposerLoggingException: " + errorString;
        }
    }

    /**
@@ -179,7 +207,10 @@ public class CallLog {
     * @hide
     */
    @SystemApi
    @RequiresPermission(Manifest.permission.WRITE_CALL_LOG)
    @RequiresPermission(allOf = {
            Manifest.permission.WRITE_CALL_LOG,
            Manifest.permission.INTERACT_ACROSS_USERS
    })
    public static void storeCallComposerPictureAsUser(@NonNull Context context,
            @Nullable UserHandle user,
            @NonNull InputStream input,
@@ -191,69 +222,164 @@ public class CallLog {
        Objects.requireNonNull(callback);

        executor.execute(() -> {
            ByteArrayOutputStream tmpOut = new ByteArrayOutputStream();

            // Read the entire input into memory first in case we have to write multiple times and
            // the input isn't resettable.
            byte[] buffer = new byte[1024];
            int bytesRead;
            while (true) {
                try {
                    bytesRead = input.read(buffer);
                } catch (IOException e) {
                    Log.e(LOG_TAG, "IOException while reading call composer pic from input: "
                            + e);
                    callback.onError(new CallComposerLoggingException(
                            CallComposerLoggingException.ERROR_INPUT_CLOSED));
                    return;
                }
                if (bytesRead < 0) {
                    break;
                }
                tmpOut.write(buffer, 0, bytesRead);
            }
            byte[] picData = tmpOut.toByteArray();

            UserManager userManager = context.getSystemService(UserManager.class);
            // Nasty casework for the shadow calllog begins...
            // First see if we're just inserting for one user. If so, insert into the shadow
            // based on whether that user is unlocked.
            if (user != null) {
                Uri baseUri = userManager.isUserUnlocked(user) ? CALL_COMPOSER_PICTURE_URI
                        : SHADOW_CALL_COMPOSER_PICTURE_URI;
                Uri pictureInsertionUri = ContentProvider.maybeAddUserId(baseUri,
                        user.getIdentifier());
                Log.i(LOG_TAG, "Inserting call composer for single user at "
                        + pictureInsertionUri);

                try {
                    Uri result = storeCallComposerPictureAtUri(
                            context, pictureInsertionUri, false, picData);
                    callback.onResult(result);
                } catch (CallComposerLoggingException e) {
                    callback.onError(e);
                }
                return;
            }

            // Next, see if the system user is locked. If so, only insert to the system shadow
            if (!userManager.isUserUnlocked(UserHandle.SYSTEM)) {
                Uri pictureInsertionUri = ContentProvider.maybeAddUserId(
                        SHADOW_CALL_COMPOSER_PICTURE_URI,
                        UserHandle.SYSTEM.getIdentifier());
                Log.i(LOG_TAG, "Inserting call composer for all users, but system locked at "
                        + pictureInsertionUri);
                try {
                    Uri result =
                            storeCallComposerPictureAtUri(context, pictureInsertionUri,
                                    true, picData);
                    callback.onResult(result);
                } catch (CallComposerLoggingException e) {
                    callback.onError(e);
                }
                return;
            }

            // If we're inserting to all users and the system user is unlocked, then insert to all
            // running users. Non running/still locked users will copy from the system when they
            // start.
            // First, insert to the system calllog to get the basename to use for the rest of the
            // users.
            Uri systemPictureInsertionUri = ContentProvider.maybeAddUserId(
                    CALL_COMPOSER_PICTURE_URI,
                    UserHandle.SYSTEM.getIdentifier());
            Uri systemInsertedPicture;
            try {
                systemInsertedPicture =
                        storeCallComposerPictureAtUri(context, systemPictureInsertionUri,
                                true, picData);
                Log.i(LOG_TAG, "Inserting call composer for all users, succeeded with system,"
                        + " result is " + systemInsertedPicture);
            } catch (CallComposerLoggingException e) {
                callback.onError(e);
                return;
            }

            // Next, insert into all users that have call log access AND are running AND are
            // decrypted.
            Uri strippedInsertionUri = ContentProvider.getUriWithoutUserId(systemInsertedPicture);
            for (UserInfo u : userManager.getAliveUsers()) {
                UserHandle userHandle = u.getUserHandle();
                if (userHandle.isSystem()) {
                    // Already written.
                    continue;
                }

                if (!Calls.shouldHaveSharedCallLogEntries(
                        context, userManager, userHandle.getIdentifier())) {
                    // Shouldn't have calllog entries.
                    continue;
                }

                if (userManager.isUserRunning(userHandle)
                        && userManager.isUserUnlocked(userHandle)) {
                    Uri insertionUri = ContentProvider.maybeAddUserId(strippedInsertionUri,
                            userHandle.getIdentifier());
                    Log.i(LOG_TAG, "Inserting call composer for all users, now on user "
                            + userHandle + " inserting at " + insertionUri);
                    try {
                        storeCallComposerPictureAtUri(context, insertionUri, false, picData);
                    } catch (CallComposerLoggingException e) {
                        Log.e(LOG_TAG, "Error writing for user " + userHandle.getIdentifier()
                                + ": " + e);
                        // If one or more users failed but the system user succeeded, don't return
                        // an error -- the image is still around somewhere, and we'll be able to
                        // find it in the system user's call log if needed.
                    }
                }
            }
            callback.onResult(strippedInsertionUri);
        });
    }

    private static Uri storeCallComposerPictureAtUri(
            Context context, Uri insertionUri,
            boolean forAllUsers, byte[] picData) throws CallComposerLoggingException {
        Uri pictureFileUri;
            Uri pictureInsertionUri = context.getSystemService(UserManager.class)
                    .isUserUnlocked() ? CALL_COMPOSER_PICTURE_URI
                    : CALL_COMPOSER_PICTURE_URI.buildUpon().authority(SHADOW_AUTHORITY).build();
        try {
                // ContentResolver#insert says that the second argument is nullable. It is in fact
                // not nullable.
            ContentValues cv = new ContentValues();
                pictureFileUri = context.getContentResolver().insert(pictureInsertionUri, cv);
            cv.put(Calls.ADD_FOR_ALL_USERS, forAllUsers ? 1 : 0);
            pictureFileUri = context.getContentResolver().insert(insertionUri, cv);
        } catch (ParcelableException e) {
            // Most likely an IOException. We don't have a good way of distinguishing them so
            // just return an unknown error.
                sendCallComposerError(callback, CallComposerLoggingException.ERROR_UNKNOWN);
                return;
            throw new CallComposerLoggingException(CallComposerLoggingException.ERROR_UNKNOWN);
        }
        if (pictureFileUri == null) {
            // If the call log provider returns null, it means that there's not enough space
            // left to store the maximum-sized call composer image.
                sendCallComposerError(callback, CallComposerLoggingException.ERROR_STORAGE_FULL);
                return;
            throw new CallComposerLoggingException(CallComposerLoggingException.ERROR_STORAGE_FULL);
        }

            boolean wroteSuccessfully = false;
        try (ParcelFileDescriptor pfd =
                     context.getContentResolver().openFileDescriptor(pictureFileUri, "w")) {
            FileOutputStream output = new FileOutputStream(pfd.getFileDescriptor());
                byte[] buffer = new byte[1024];
                int bytesRead;
                while (true) {
            try {
                        bytesRead = input.read(buffer);
                output.write(picData);
            } catch (IOException e) {
                        sendCallComposerError(callback,
                                CallComposerLoggingException.ERROR_INPUT_CLOSED);
                        throw e;
                    }
                    if (bytesRead < 0) {
                        break;
                    }
                    try {
                        output.write(buffer, 0, bytesRead);
                    } catch (IOException e) {
                        sendCallComposerError(callback,
                Log.e(LOG_TAG, "Got IOException writing to remote end: " + e);
                // Clean up our mess if we didn't successfully write the file.
                context.getContentResolver().delete(pictureFileUri, null);
                throw new CallComposerLoggingException(
                        CallComposerLoggingException.ERROR_REMOTE_END_CLOSED);
                        throw e;
                    }
            }
                wroteSuccessfully = true;
        } catch (FileNotFoundException e) {
                callback.onError(new CallComposerLoggingException(
                        CallComposerLoggingException.ERROR_UNKNOWN));
            throw new CallComposerLoggingException(CallComposerLoggingException.ERROR_UNKNOWN);
        } catch (IOException e) {
                Log.e(LOG_TAG, "IOException while writing call composer pic to call log: "
                        + e);
            }

            if (wroteSuccessfully) {
                callback.onResult(pictureFileUri);
            } else {
                // Clean up our mess if we didn't successfully write the file.
                context.getContentResolver().delete(pictureFileUri, null);
            // Ignore, this is only thrown upon closing.
            Log.e(LOG_TAG, "Got IOException closing remote descriptor: " + e);
        }
        });
        return pictureFileUri;
    }

    // Only call on the correct executor.