Merge "Add support for audio-focused notifications."

This commit is contained in:
Shane Brennan
2017-01-19 00:12:06 +00:00
committed by Android (Google) Code Review
5 changed files with 272 additions and 31 deletions

View File

@@ -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<java.lang.String, android.net.Uri>);
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<java.lang.String> getAllowedDataTypes();
method public java.lang.CharSequence[] getChoices();
method public static java.util.Map<java.lang.String, android.net.Uri> 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<android.app.RemoteInput> 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);

View File

@@ -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<java.lang.String, android.net.Uri>);
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<java.lang.String> getAllowedDataTypes();
method public java.lang.CharSequence[] getChoices();
method public static java.util.Map<java.lang.String, android.net.Uri> 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<android.app.RemoteInput> 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);

View File

@@ -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<java.lang.String, android.net.Uri>);
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<java.lang.String> getAllowedDataTypes();
method public java.lang.CharSequence[] getChoices();
method public static java.util.Map<java.lang.String, android.net.Uri> 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<android.app.RemoteInput> 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);

View File

@@ -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:
* <pre>
* {@code
* Notification.Builder myBuilder = (build your Notification as normal);
* myBuilder.getExtras().putString(EXTRA_AUDIO_CONTENTS_URI, myAudioUri.toString());
* }
* </pre>
*/
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<RemoteInput> dataOnlyInputs = new ArrayList<>();
RemoteInput[] previousDataInputs =
(RemoteInput[]) mExtras.getParcelableArray(EXTRA_DATA_ONLY_INPUTS);
if (previousDataInputs == null) {
for (RemoteInput input : previousDataInputs) {
dataOnlyInputs.add(input);
}
}
List<RemoteInput> 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);
}
}

View File

@@ -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<String> mAllowedDataTypes;
private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
int flags, Bundle extras) {
int flags, Bundle extras, ArraySet<String> 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<String> 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<String> 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<String>) 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:
* <pre>
* {@code
* Map<String, Uri> results =
* RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
* if (results != null) {
* Uri data = results.get(MIME_TYPE_OF_INTEREST);
* }
* }
* </pre>
* @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<String, Uri> getDataResultsFromIntent(
Intent intent, String remoteInputResultKey) {
Intent clipDataIntent = getClipDataIntentFromIntent(intent);
if (clipDataIntent == null) {
return null;
}
Map<String, Uri> 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<String, Uri> results) {
Intent clipDataIntent = getClipDataIntentFromIntent(intent);
if (clipDataIntent == null) {
clipDataIntent = new Intent(); // First time we've added a result.
}
for (Map.Entry<String, Uri> 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<RemoteInput> CREATOR = new Creator<RemoteInput>() {
@@ -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();
}
}