Loading core/api/system-current.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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 { core/java/android/provider/CallLog.java +177 −51 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; } } /** Loading @@ -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, Loading @@ -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. Loading Loading
core/api/system-current.txt +1 −1 Original line number Diff line number Diff line Loading @@ -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 {
core/java/android/provider/CallLog.java +177 −51 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading Loading @@ -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; } } /** Loading @@ -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, Loading @@ -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. Loading