diff --git a/api/current.txt b/api/current.txt index b812f1b6119e6..8d281a2f61316 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5027,6 +5027,7 @@ package android.app { field public static final int DEFAULT_LIGHTS = 4; // 0x4 field public static final int DEFAULT_SOUND = 1; // 0x1 field public static final int DEFAULT_VIBRATE = 2; // 0x2 + field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText"; field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; @@ -5110,6 +5111,7 @@ package android.app { method public android.app.Notification.Action clone(); method public int describeContents(); method public boolean getAllowGeneratedReplies(); + method public android.app.RemoteInput[] getDataOnlyRemoteInputs(); method public android.os.Bundle getExtras(); method public android.graphics.drawable.Icon getIcon(); method public android.app.RemoteInput[] getRemoteInputs(); @@ -5576,14 +5578,18 @@ package android.app { } public final class RemoteInput implements android.os.Parcelable { + method public static void addDataResultToIntent(android.app.RemoteInput, android.content.Intent, java.util.Map); method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); method public boolean getAllowFreeFormInput(); + method public java.util.Set getAllowedDataTypes(); method public java.lang.CharSequence[] getChoices(); + method public static java.util.Map getDataResultsFromIntent(android.content.Intent, java.lang.String); method public android.os.Bundle getExtras(); method public java.lang.CharSequence getLabel(); method public java.lang.String getResultKey(); method public static android.os.Bundle getResultsFromIntent(android.content.Intent); + method public boolean isDataOnly(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; field public static final java.lang.String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; @@ -5595,6 +5601,7 @@ package android.app { method public android.app.RemoteInput.Builder addExtras(android.os.Bundle); method public android.app.RemoteInput build(); method public android.os.Bundle getExtras(); + method public android.app.RemoteInput.Builder setAllowDataType(java.lang.String, boolean); method public android.app.RemoteInput.Builder setAllowFreeFormInput(boolean); method public android.app.RemoteInput.Builder setChoices(java.lang.CharSequence[]); method public android.app.RemoteInput.Builder setLabel(java.lang.CharSequence); diff --git a/api/system-current.txt b/api/system-current.txt index cbb7f86125b0d..d7a8fc5ef87a7 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5185,6 +5185,7 @@ package android.app { field public static final int DEFAULT_LIGHTS = 4; // 0x4 field public static final int DEFAULT_SOUND = 1; // 0x1 field public static final int DEFAULT_VIBRATE = 2; // 0x2 + field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText"; field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; @@ -5270,6 +5271,7 @@ package android.app { method public android.app.Notification.Action clone(); method public int describeContents(); method public boolean getAllowGeneratedReplies(); + method public android.app.RemoteInput[] getDataOnlyRemoteInputs(); method public android.os.Bundle getExtras(); method public android.graphics.drawable.Icon getIcon(); method public android.app.RemoteInput[] getRemoteInputs(); @@ -5761,14 +5763,18 @@ package android.app { } public final class RemoteInput implements android.os.Parcelable { + method public static void addDataResultToIntent(android.app.RemoteInput, android.content.Intent, java.util.Map); method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); method public boolean getAllowFreeFormInput(); + method public java.util.Set getAllowedDataTypes(); method public java.lang.CharSequence[] getChoices(); + method public static java.util.Map getDataResultsFromIntent(android.content.Intent, java.lang.String); method public android.os.Bundle getExtras(); method public java.lang.CharSequence getLabel(); method public java.lang.String getResultKey(); method public static android.os.Bundle getResultsFromIntent(android.content.Intent); + method public boolean isDataOnly(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; field public static final java.lang.String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; @@ -5780,6 +5786,7 @@ package android.app { method public android.app.RemoteInput.Builder addExtras(android.os.Bundle); method public android.app.RemoteInput build(); method public android.os.Bundle getExtras(); + method public android.app.RemoteInput.Builder setAllowDataType(java.lang.String, boolean); method public android.app.RemoteInput.Builder setAllowFreeFormInput(boolean); method public android.app.RemoteInput.Builder setChoices(java.lang.CharSequence[]); method public android.app.RemoteInput.Builder setLabel(java.lang.CharSequence); diff --git a/api/test-current.txt b/api/test-current.txt index 8e2f781477815..807d6bd095f7d 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -5037,6 +5037,7 @@ package android.app { field public static final int DEFAULT_LIGHTS = 4; // 0x4 field public static final int DEFAULT_SOUND = 1; // 0x1 field public static final int DEFAULT_VIBRATE = 2; // 0x2 + field public static final java.lang.String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; field public static final java.lang.String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; field public static final java.lang.String EXTRA_BIG_TEXT = "android.bigText"; field public static final java.lang.String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; @@ -5120,6 +5121,7 @@ package android.app { method public android.app.Notification.Action clone(); method public int describeContents(); method public boolean getAllowGeneratedReplies(); + method public android.app.RemoteInput[] getDataOnlyRemoteInputs(); method public android.os.Bundle getExtras(); method public android.graphics.drawable.Icon getIcon(); method public android.app.RemoteInput[] getRemoteInputs(); @@ -5587,14 +5589,18 @@ package android.app { } public final class RemoteInput implements android.os.Parcelable { + method public static void addDataResultToIntent(android.app.RemoteInput, android.content.Intent, java.util.Map); method public static void addResultsToIntent(android.app.RemoteInput[], android.content.Intent, android.os.Bundle); method public int describeContents(); method public boolean getAllowFreeFormInput(); + method public java.util.Set getAllowedDataTypes(); method public java.lang.CharSequence[] getChoices(); + method public static java.util.Map getDataResultsFromIntent(android.content.Intent, java.lang.String); method public android.os.Bundle getExtras(); method public java.lang.CharSequence getLabel(); method public java.lang.String getResultKey(); method public static android.os.Bundle getResultsFromIntent(android.content.Intent); + method public boolean isDataOnly(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; field public static final java.lang.String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; @@ -5606,6 +5612,7 @@ package android.app { method public android.app.RemoteInput.Builder addExtras(android.os.Bundle); method public android.app.RemoteInput build(); method public android.os.Bundle getExtras(); + method public android.app.RemoteInput.Builder setAllowDataType(java.lang.String, boolean); method public android.app.RemoteInput.Builder setAllowFreeFormInput(boolean); method public android.app.RemoteInput.Builder setChoices(java.lang.CharSequence[]); method public android.app.RemoteInput.Builder setLabel(java.lang.CharSequence); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index c6f0b661a65ac..ab0b68d61df5b 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -988,6 +988,32 @@ public class Notification implements Parcelable */ public static final String EXTRA_CONTAINS_CUSTOM_VIEW = "android.contains.customView"; + /** + * {@link #extras} key: the audio contents of this notification. + * + * This is for use when rendering the notification on an audio-focused interface; + * the audio contents are a complete sound sample that contains the contents/body of the + * notification. This may be used in substitute of a Text-to-Speech reading of the + * notification. For example if the notification represents a voice message this should point + * to the audio of that message. + * + * The data stored under this key should be a String representation of a Uri that contains the + * audio contents in one of the following formats: WAV, PCM 16-bit, AMR-WB. + * + * This extra is unnecessary if you are using {@code MessagingStyle} since each {@code Message} + * has a field for holding data URI. That field can be used for audio. + * See {@code Message#setData}. + * + * Example usage: + *
+     * {@code
+     * Notification.Builder myBuilder = (build your Notification as normal);
+     * myBuilder.getExtras().putString(EXTRA_AUDIO_CONTENTS_URI, myAudioUri.toString());
+     * }
+     * 
+ */ + public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; + /** @hide */ @SystemApi public static final String EXTRA_SUBSTITUTE_APP_NAME = "android.substName"; @@ -1007,6 +1033,21 @@ public class Notification implements Parcelable * to attach actions. */ public static class Action implements Parcelable { + /** + * {@link #extras} key: Keys to a {@link Parcelable} {@link ArrayList} of + * {@link RemoteInput}s. + * + * This is intended for {@link RemoteInput}s that only accept data, meaning + * {@link RemoteInput#getAllowFreeFormInput} is false, {@link RemoteInput#getChoices} + * is null or empty, and {@link RemoteInput#getAllowedDataTypes is non-null and not + * empty. These {@link RemoteInput}s will be ignored by devices that do not + * support non-text-based {@link RemoteInput}s. See {@link Builder#build}. + * + * You can test if a RemoteInput matches these constraints using + * {@link RemoteInput#isDataOnly}. + */ + private static final String EXTRA_DATA_ONLY_INPUTS = "android.extra.DATA_ONLY_INPUTS"; + private final Bundle mExtras; private Icon mIcon; private final RemoteInput[] mRemoteInputs; @@ -1097,12 +1138,27 @@ public class Notification implements Parcelable /** * Get the list of inputs to be collected from the user when this action is sent. - * May return null if no remote inputs were added. + * May return null if no remote inputs were added. Only returns inputs which accept + * a text input. For inputs which only accept data use {@link #getDataOnlyRemoteInputs}. */ public RemoteInput[] getRemoteInputs() { return mRemoteInputs; } + /** + * Get the list of inputs to be collected from the user that ONLY accept data when this + * action is sent. These remote inputs are guaranteed to return true on a call to + * {@link RemoteInput#isDataOnly}. + * + * May return null if no data-only remote inputs were added. + * + * This method exists so that legacy RemoteInput collectors that pre-date the addition + * of non-textual RemoteInputs do not access these remote inputs. + */ + public RemoteInput[] getDataOnlyRemoteInputs() { + return (RemoteInput[]) mExtras.getParcelableArray(EXTRA_DATA_ONLY_INPUTS); + } + /** * Builder class for {@link Action} objects. */ @@ -1226,9 +1282,32 @@ public class Notification implements Parcelable * @return the built action */ public Action build() { - RemoteInput[] remoteInputs = mRemoteInputs != null - ? mRemoteInputs.toArray(new RemoteInput[mRemoteInputs.size()]) : null; - return new Action(mIcon, mTitle, mIntent, mExtras, remoteInputs, + ArrayList dataOnlyInputs = new ArrayList<>(); + RemoteInput[] previousDataInputs = + (RemoteInput[]) mExtras.getParcelableArray(EXTRA_DATA_ONLY_INPUTS); + if (previousDataInputs == null) { + for (RemoteInput input : previousDataInputs) { + dataOnlyInputs.add(input); + } + } + List textInputs = new ArrayList<>(); + if (mRemoteInputs != null) { + for (RemoteInput input : mRemoteInputs) { + if (input.isDataOnly()) { + dataOnlyInputs.add(input); + } else { + textInputs.add(input); + } + } + } + if (!dataOnlyInputs.isEmpty()) { + RemoteInput[] dataInputsArr = + dataOnlyInputs.toArray(new RemoteInput[dataOnlyInputs.size()]); + mExtras.putParcelableArray(EXTRA_DATA_ONLY_INPUTS, dataInputsArr); + } + RemoteInput[] textInputsArr = textInputs.isEmpty() + ? null : textInputs.toArray(new RemoteInput[textInputs.size()]); + return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr, mAllowGeneratedReplies); } } diff --git a/core/java/android/app/RemoteInput.java b/core/java/android/app/RemoteInput.java index 11420c5ed4ffc..d1dc859314047 100644 --- a/core/java/android/app/RemoteInput.java +++ b/core/java/android/app/RemoteInput.java @@ -19,9 +19,14 @@ package android.app; import android.content.ClipData; import android.content.ClipDescription; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArraySet; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; /** * A {@code RemoteInput} object specifies input to be collected from a user to be passed along with @@ -61,9 +66,13 @@ public final class RemoteInput implements Parcelable { /** Label used to denote the clip data type used for remote input transport */ public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results"; - /** Extra added to a clip data intent object to hold the results bundle. */ + /** Extra added to a clip data intent object to hold the text results bundle. */ public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; + /** Extra added to a clip data intent object to hold the data results bundle. */ + private static final String EXTRA_DATA_TYPE_RESULTS_DATA = + "android.remoteinput.dataTypeResultsData"; + // Flags bitwise-ored to mFlags private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1; @@ -75,14 +84,16 @@ public final class RemoteInput implements Parcelable { private final CharSequence[] mChoices; private final int mFlags; private final Bundle mExtras; + private final ArraySet mAllowedDataTypes; private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, - int flags, Bundle extras) { + int flags, Bundle extras, ArraySet allowedDataTypes) { this.mResultKey = resultKey; this.mLabel = label; this.mChoices = choices; this.mFlags = flags; this.mExtras = extras; + this.mAllowedDataTypes = allowedDataTypes; } /** @@ -107,6 +118,21 @@ public final class RemoteInput implements Parcelable { return mChoices; } + public Set getAllowedDataTypes() { + return mAllowedDataTypes; + } + + /** + * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput} + * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes is + * non-null and not empty. + */ + public boolean isDataOnly() { + return !getAllowFreeFormInput() + && (getChoices() == null || getChoices().length == 0) + && !getAllowedDataTypes().isEmpty(); + } + /** * Get whether or not users can provide an arbitrary value for * input. If you set this to {@code false}, users must select one of the @@ -133,6 +159,7 @@ public final class RemoteInput implements Parcelable { private CharSequence[] mChoices; private int mFlags = DEFAULT_FLAGS; private Bundle mExtras = new Bundle(); + private final ArraySet mAllowedDataTypes = new ArraySet<>(); /** * Create a builder object for {@link RemoteInput} objects. @@ -177,14 +204,34 @@ public final class RemoteInput implements Parcelable { /** * Specifies whether the user can provide arbitrary values. * - * @param allowFreeFormInput The default is {@code true}. - * If you specify {@code false}, you must provide a non-null - * and non-empty array to {@link #setChoices} or an + * @param mimeType A mime type that results are allowed to come in. + * Be aware that text results (see {@link #setAllowFreeFormInput} + * are allowed by default. If you do not want text results you will have to + * pass false to {@code setAllowFreeFormInput}. + * @param doAllow Whether the mime type should be allowed or not. + * @return this object for method chaining + */ + public Builder setAllowDataType(String mimeType, boolean doAllow) { + if (doAllow) { + mAllowedDataTypes.add(mimeType); + } else { + mAllowedDataTypes.remove(mimeType); + } + return this; + } + + /** + * Specifies whether the user can provide arbitrary text values. + * + * @param allowFreeFormTextInput The default is {@code true}. + * If you specify {@code false}, you must either provide a non-null + * and non-empty array to {@link #setChoices}, or enable a data result + * in {@code setAllowDataType}. Otherwise an * {@link IllegalArgumentException} is thrown. * @return this object for method chaining */ - public Builder setAllowFreeFormInput(boolean allowFreeFormInput) { - setFlag(mFlags, allowFreeFormInput); + public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) { + setFlag(mFlags, allowFreeFormTextInput); return this; } @@ -224,7 +271,8 @@ public final class RemoteInput implements Parcelable { * object. */ public RemoteInput build() { - return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mExtras); + return new RemoteInput( + mResultKey, mLabel, mChoices, mFlags, mExtras, mAllowedDataTypes); } } @@ -234,32 +282,68 @@ public final class RemoteInput implements Parcelable { mChoices = in.readCharSequenceArray(); mFlags = in.readInt(); mExtras = in.readBundle(); + mAllowedDataTypes = (ArraySet) in.readArraySet(null); } /** - * Get the remote input results bundle from an intent. The returned Bundle will - * contain a key/value for every result key populated by remote input collector. - * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. + * Similar as {@link #getResultsFromIntent} but retrieves data results for a + * specific RemoteInput result. To retrieve a value use: + *
+     * {@code
+     * Map results =
+     *     RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
+     * if (results != null) {
+     *   Uri data = results.get(MIME_TYPE_OF_INTEREST);
+     * }
+     * }
+     * 
+ * @param intent The intent object that fired in response to an action or content intent + * which also had one or more remote input requested. + * @param remoteInputResultKey The result key for the RemoteInput you want results for. + */ + public static Map getDataResultsFromIntent( + Intent intent, String remoteInputResultKey) { + Intent clipDataIntent = getClipDataIntentFromIntent(intent); + if (clipDataIntent == null) { + return null; + } + Map results = new HashMap<>(); + Bundle extras = clipDataIntent.getExtras(); + for (String key : extras.keySet()) { + if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) { + String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length()); + if (mimeType == null || mimeType.isEmpty()) { + continue; + } + Bundle bundle = clipDataIntent.getBundleExtra(key); + String uriStr = bundle.getString(remoteInputResultKey); + if (uriStr == null || uriStr.isEmpty()) { + continue; + } + results.put(mimeType, Uri.parse(uriStr)); + } + } + return results.isEmpty() ? null : results; + } + + /** + * Get the remote input text results bundle from an intent. The returned Bundle will + * contain a key/value for every result key populated with text by remote input collector. + * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For non-text + * results use {@link #getDataResultsFromIntent}. * @param intent The intent object that fired in response to an action or content intent * which also had one or more remote input requested. */ public static Bundle getResultsFromIntent(Intent intent) { - ClipData clipData = intent.getClipData(); - if (clipData == null) { + Intent clipDataIntent = getClipDataIntentFromIntent(intent); + if (clipDataIntent == null) { return null; } - ClipDescription clipDescription = clipData.getDescription(); - if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { - return null; - } - if (clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) { - return clipData.getItemAt(0).getIntent().getExtras().getParcelable(EXTRA_RESULTS_DATA); - } - return null; + return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA); } /** - * Populate an intent object with the results gathered from remote input. This method + * Populate an intent object with the text results gathered from remote input. This method * should only be called by remote input collection services when sending results to a * pending intent. * @param remoteInputs The remote inputs for which results are being provided @@ -267,20 +351,61 @@ public final class RemoteInput implements Parcelable { * field of the intent will be modified to contain the results. * @param results A bundle holding the remote input results. This bundle should * be populated with keys matching the result keys specified in - * {@code remoteInputs} with values being the result per key. + * {@code remoteInputs} with values being the CharSequence results per key. */ public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { - Bundle resultsBundle = new Bundle(); + Intent clipDataIntent = getClipDataIntentFromIntent(intent); + if (clipDataIntent == null) { + clipDataIntent = new Intent(); // First time we've added a result. + } + Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA); + if (resultsBundle == null) { + resultsBundle = new Bundle(); + } for (RemoteInput remoteInput : remoteInputs) { Object result = results.get(remoteInput.getResultKey()); if (result instanceof CharSequence) { resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result); } } - Intent clipIntent = new Intent(); - clipIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle); - intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipIntent)); + clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle); + intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent)); + } + + /** + * Same as {@link #addResultsToIntent} but for setting data results. + * @param remoteInput The remote input for which results are being provided + * @param intent The intent to add remote input results to. The {@link ClipData} + * field of the intent will be modified to contain the results. + * @param results A map of mime type to the Uri result for that mime type. + */ + public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent, + Map results) { + Intent clipDataIntent = getClipDataIntentFromIntent(intent); + if (clipDataIntent == null) { + clipDataIntent = new Intent(); // First time we've added a result. + } + for (Map.Entry entry : results.entrySet()) { + String mimeType = entry.getKey(); + Uri uri = entry.getValue(); + if (mimeType == null) { + continue; + } + Bundle resultsBundle = + clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType)); + if (resultsBundle == null) { + resultsBundle = new Bundle(); + } + resultsBundle.putString(remoteInput.getResultKey(), uri.toString()); + + clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle); + } + intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent)); + } + + private static String getExtraResultsKeyForData(String mimeType) { + return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType; } @Override @@ -295,6 +420,7 @@ public final class RemoteInput implements Parcelable { out.writeCharSequenceArray(mChoices); out.writeInt(mFlags); out.writeBundle(mExtras); + out.writeArraySet(mAllowedDataTypes); } public static final Creator CREATOR = new Creator() { @@ -308,4 +434,19 @@ public final class RemoteInput implements Parcelable { return new RemoteInput[size]; } }; + + private static Intent getClipDataIntentFromIntent(Intent intent) { + ClipData clipData = intent.getClipData(); + if (clipData == null) { + return null; + } + ClipDescription clipDescription = clipData.getDescription(); + if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { + return null; + } + if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) { + return null; + } + return clipData.getItemAt(0).getIntent(); + } }