summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Hall Liu <hallliu@google.com> 2020-12-03 16:45:40 -0800
committer Hall Liu <hallliu@google.com> 2020-12-08 16:35:48 -0800
commitd5c67fd3b2624c2b3b5764af8aae892d4effa011 (patch)
tree5c603944244bbe00ba3e819468347bdfeb37e1a8
parentd6ea9e538a09688c9c0f7fc11f1f844f3db6633e (diff)
Add APIs for storing pictures in the call log
Add APIs for Telephony to store pictures for call composer in the call log. Test: atest android.provider.cts.contacts.CallLogTest#testCallComposerImageStorage Bug: 174798736 Change-Id: I5baf6d3751f3ed6edc77515e5eeb97906dc66758
-rw-r--r--core/api/current.txt1
-rw-r--r--core/api/system-current.txt12
-rw-r--r--core/java/android/provider/CallLog.java188
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java17
4 files changed, 217 insertions, 1 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 8a6430febf91..cdf644fb6553 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -46977,6 +46977,7 @@ package android.telephony {
method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRECISE_PHONE_STATE) public String getManualNetworkSelectionPlmn();
method @Nullable public String getManufacturerCode();
method @Nullable public String getManufacturerCode(int);
+ method public static long getMaximumCallComposerPictureSize();
method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public String getMeid();
method @RequiresPermission("android.permission.READ_PRIVILEGED_PHONE_STATE") public String getMeid(int);
method public String getMmsUAProfUrl();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 66d29d375a7c..91f49dccbd19 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -8380,6 +8380,18 @@ 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>);
+ }
+
+ public static class CallLog.CallComposerLoggingException extends java.lang.Throwable {
+ 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
+ field public static final int ERROR_STORAGE_FULL = 2; // 0x2
+ field public static final int ERROR_UNKNOWN = 0; // 0x0
+ }
+
@Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns {
field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata";
field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata";
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 105ffaa4718e..d1aa48914e70 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -17,7 +17,14 @@
package android.provider;
+import android.Manifest;
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -30,6 +37,9 @@ import android.location.Country;
import android.location.CountryDetector;
import android.net.Uri;
import android.os.Build;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.os.ParcelableException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.ContactsContract.CommonDataKinds.Callable;
@@ -44,9 +54,15 @@ import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
/**
* The CallLog provider contains information about placed and received calls.
@@ -63,6 +79,12 @@ public class CallLog {
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY);
+ /** @hide */
+ public static final String CALL_COMPOSER_SEGMENT = "call_composer";
+
+ /** @hide */
+ public static final Uri CALL_COMPOSER_PICTURE_URI =
+ CONTENT_URI.buildUpon().appendPath(CALL_COMPOSER_SEGMENT).build();
/**
* The "shadow" provider stores calllog when the real calllog provider is encrypted. The
@@ -75,6 +97,172 @@ public class CallLog {
public static final String SHADOW_AUTHORITY = "call_log_shadow";
/**
+ * Describes an error encountered while storing a call composer picture in the call log.
+ * @hide
+ */
+ @SystemApi
+ public static class CallComposerLoggingException extends Throwable {
+ /**
+ * Indicates an unknown error.
+ */
+ public static final int ERROR_UNKNOWN = 0;
+
+ /**
+ * Indicates that the process hosting the call log died or otherwise encountered an
+ * unrecoverable error while storing the picture.
+ *
+ * The caller should retry if this error is encountered.
+ */
+ public static final int ERROR_REMOTE_END_CLOSED = 1;
+
+ /**
+ * Indicates that the device has insufficient space to store this picture.
+ *
+ * The caller should not retry if this error is encountered.
+ */
+ public static final int ERROR_STORAGE_FULL = 2;
+
+ /**
+ * Indicates that the {@link InputStream} passed to {@link #storeCallComposerPictureAsUser}
+ * 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 = 3;
+
+ /** @hide */
+ @IntDef(prefix = {"ERROR_"}, value = {
+ ERROR_UNKNOWN,
+ ERROR_REMOTE_END_CLOSED,
+ ERROR_STORAGE_FULL,
+ ERROR_INPUT_CLOSED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallComposerLoggingError { }
+
+ private final int mErrorCode;
+
+ /** @hide */
+ public CallComposerLoggingException(@CallComposerLoggingError int errorCode) {
+ mErrorCode = errorCode;
+ }
+
+ /**
+ * @return The error code for this exception.
+ */
+ public @CallComposerLoggingError int getErrorCode() {
+ return mErrorCode;
+ }
+ }
+
+ /**
+ * Supplies a call composer picture to the call log for persistent storage.
+ *
+ * This method is used by Telephony to store pictures selected by the user or sent from the
+ * remote party as part of a voice call with call composer. The {@link Uri} supplied in the
+ * callback can be used to retrieve the image via {@link ContentResolver#openFile} or stored in
+ * the {@link Calls} table in the TODO: link column name.
+ *
+ * The caller is responsible for closing the {@link InputStream} after the callback indicating
+ * success or failure.
+ *
+ * @param context An instance of {@link Context}.
+ * @param user The user for whom the picture is stored. If {@code null}, the picture will be
+ * stored for all users.
+ * @param input An input stream from which the picture to store should be read. The input data
+ * must be decodeable as either a JPEG, PNG, or GIF image.
+ * @param executor The {@link Executor} on which to perform the file transfer operation and
+ * call the supplied callback.
+ * @param callback Callback that's called after the picture is successfully stored or when an
+ * error occurs.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(Manifest.permission.WRITE_CALL_LOG)
+ public static void storeCallComposerPictureAsUser(@NonNull Context context,
+ @Nullable UserHandle user,
+ @NonNull InputStream input,
+ @CallbackExecutor @NonNull Executor executor,
+ @NonNull OutcomeReceiver<Uri, CallComposerLoggingException> callback) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(input);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+
+ executor.execute(() -> {
+ 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);
+ } 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;
+ }
+ 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;
+ }
+
+ 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);
+ } 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,
+ CallComposerLoggingException.ERROR_REMOTE_END_CLOSED);
+ throw e;
+ }
+ }
+ wroteSuccessfully = true;
+ } catch (FileNotFoundException e) {
+ callback.onError(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);
+ }
+ });
+ }
+
+ // Only call on the correct executor.
+ private static void sendCallComposerError(OutcomeReceiver<?, CallComposerLoggingException> cb,
+ int error) {
+ cb.onError(new CallComposerLoggingException(error));
+ }
+
+ /**
* Contains the recent calls.
*/
public static class Calls implements BaseColumns {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 239329cb447e..4a3e1901400f 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -23,6 +23,7 @@ import static android.provider.Telephony.Carriers.INVALID_APN_ID;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.Manifest;
+import android.annotation.BytesLong;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.LongDef;
@@ -124,7 +125,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
-import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -549,6 +549,21 @@ public class TelephonyManager {
return getPhoneCount() > 1;
}
+ 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.
+ *
+ * @return Maximum file size in bytes.
+ */
+ public static @BytesLong long getMaximumCallComposerPictureSize() {
+ return MAXIMUM_CALL_COMPOSER_PICTURE_SIZE;
+ }
+
//
// Broadcast Intent actions
//