Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.app;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.content.ClipData;
     23 import android.content.ClipDescription;
     24 import android.content.Intent;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.os.Parcel;
     28 import android.os.Parcelable;
     29 import android.util.ArraySet;
     30 
     31 import java.lang.annotation.Retention;
     32 import java.lang.annotation.RetentionPolicy;
     33 import java.util.HashMap;
     34 import java.util.Map;
     35 import java.util.Set;
     36 
     37 /**
     38  * A {@code RemoteInput} object specifies input to be collected from a user to be passed along with
     39  * an intent inside a {@link android.app.PendingIntent} that is sent.
     40  * Always use {@link RemoteInput.Builder} to create instances of this class.
     41  * <p class="note"> See
     42  * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html#direct">Replying
     43  * to notifications</a> for more information on how to use this class.
     44  *
     45  * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action},
     46  * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}.
     47  * Users are prompted to input a response when they trigger the action. The results are sent along
     48  * with the intent and can be retrieved with the result key (provided to the {@link Builder}
     49  * constructor) from the Bundle returned by {@link #getResultsFromIntent}.
     50  *
     51  * <pre class="prettyprint">
     52  * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
     53  * Notification.Action action = new Notification.Action.Builder(
     54  *         R.drawable.reply, &quot;Reply&quot;, actionIntent)
     55  *         <b>.addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
     56  *                 .setLabel("Quick reply").build()</b>)
     57  *         .build();</pre>
     58  *
     59  * <p>When the {@link android.app.PendingIntent} is fired, the intent inside will contain the
     60  * input results if collected. To access these results, use the {@link #getResultsFromIntent}
     61  * function. The result values will present under the result key passed to the {@link Builder}
     62  * constructor.
     63  *
     64  * <pre class="prettyprint">
     65  * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
     66  * Bundle results = RemoteInput.getResultsFromIntent(intent);
     67  * if (results != null) {
     68  *     CharSequence quickReplyResult = results.getCharSequence(KEY_QUICK_REPLY_TEXT);
     69  * }</pre>
     70  */
     71 public final class RemoteInput implements Parcelable {
     72     /** Label used to denote the clip data type used for remote input transport */
     73     public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
     74 
     75     /** Extra added to a clip data intent object to hold the text results bundle. */
     76     public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
     77 
     78     /** Extra added to a clip data intent object to hold the data results bundle. */
     79     private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
     80             "android.remoteinput.dataTypeResultsData";
     81 
     82     /** Extra added to a clip data intent object identifying the {@link Source} of the results. */
     83     private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
     84 
     85     /** @hide */
     86     @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_FREE_FORM_INPUT, SOURCE_CHOICE})
     87     @Retention(RetentionPolicy.SOURCE)
     88     public @interface Source {}
     89 
     90     /** The user manually entered the data. */
     91     public static final int SOURCE_FREE_FORM_INPUT = 0;
     92 
     93     /** The user selected one of the choices from {@link #getChoices}. */
     94     public static final int SOURCE_CHOICE = 1;
     95 
     96     /** @hide */
     97     @IntDef(prefix = {"EDIT_CHOICES_BEFORE_SENDING_"},
     98             value = {EDIT_CHOICES_BEFORE_SENDING_AUTO, EDIT_CHOICES_BEFORE_SENDING_DISABLED,
     99                     EDIT_CHOICES_BEFORE_SENDING_ENABLED})
    100     @Retention(RetentionPolicy.SOURCE)
    101     public @interface EditChoicesBeforeSending {}
    102 
    103     /** The platform will determine whether choices will be edited before being sent to the app. */
    104     public static final int EDIT_CHOICES_BEFORE_SENDING_AUTO = 0;
    105 
    106     /** Tapping on a choice should send the input immediately, without letting the user edit it. */
    107     public static final int EDIT_CHOICES_BEFORE_SENDING_DISABLED = 1;
    108 
    109     /** Tapping on a choice should let the user edit the input before it is sent to the app. */
    110     public static final int EDIT_CHOICES_BEFORE_SENDING_ENABLED = 2;
    111 
    112     // Flags bitwise-ored to mFlags
    113     private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
    114 
    115     // Default value for flags integer
    116     private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT;
    117 
    118     private final String mResultKey;
    119     private final CharSequence mLabel;
    120     private final CharSequence[] mChoices;
    121     private final int mFlags;
    122     @EditChoicesBeforeSending private final int mEditChoicesBeforeSending;
    123     private final Bundle mExtras;
    124     private final ArraySet<String> mAllowedDataTypes;
    125 
    126     private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
    127             int flags, int editChoicesBeforeSending, Bundle extras,
    128             ArraySet<String> allowedDataTypes) {
    129         this.mResultKey = resultKey;
    130         this.mLabel = label;
    131         this.mChoices = choices;
    132         this.mFlags = flags;
    133         this.mEditChoicesBeforeSending = editChoicesBeforeSending;
    134         this.mExtras = extras;
    135         this.mAllowedDataTypes = allowedDataTypes;
    136         if (getEditChoicesBeforeSending() == EDIT_CHOICES_BEFORE_SENDING_ENABLED
    137                 && !getAllowFreeFormInput()) {
    138             throw new IllegalArgumentException(
    139                     "setEditChoicesBeforeSending requires setAllowFreeFormInput");
    140         }
    141     }
    142 
    143     /**
    144      * Get the key that the result of this input will be set in from the Bundle returned by
    145      * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
    146      */
    147     public String getResultKey() {
    148         return mResultKey;
    149     }
    150 
    151     /**
    152      * Get the label to display to users when collecting this input.
    153      */
    154     public CharSequence getLabel() {
    155         return mLabel;
    156     }
    157 
    158     /**
    159      * Get possible input choices. This can be {@code null} if there are no choices to present.
    160      */
    161     public CharSequence[] getChoices() {
    162         return mChoices;
    163     }
    164 
    165     /**
    166      * Get possible non-textual inputs that are accepted.
    167      * This can be {@code null} if the input does not accept non-textual values.
    168      * See {@link Builder#setAllowDataType}.
    169      */
    170     public Set<String> getAllowedDataTypes() {
    171         return mAllowedDataTypes;
    172     }
    173 
    174     /**
    175      * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput}
    176      * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes} is
    177      * non-null and not empty.
    178      */
    179     public boolean isDataOnly() {
    180         return !getAllowFreeFormInput()
    181                 && (getChoices() == null || getChoices().length == 0)
    182                 && !getAllowedDataTypes().isEmpty();
    183     }
    184 
    185     /**
    186      * Get whether or not users can provide an arbitrary value for
    187      * input. If you set this to {@code false}, users must select one of the
    188      * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
    189      * if you set this to false and {@link #getChoices} returns {@code null} or empty.
    190      */
    191     public boolean getAllowFreeFormInput() {
    192         return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0;
    193     }
    194 
    195     /**
    196      * Gets whether tapping on a choice should let the user edit the input before it is sent to the
    197      * app.
    198      */
    199     @EditChoicesBeforeSending
    200     public int getEditChoicesBeforeSending() {
    201         return mEditChoicesBeforeSending;
    202     }
    203 
    204     /**
    205      * Get additional metadata carried around with this remote input.
    206      */
    207     public Bundle getExtras() {
    208         return mExtras;
    209     }
    210 
    211     /**
    212      * Builder class for {@link RemoteInput} objects.
    213      */
    214     public static final class Builder {
    215         private final String mResultKey;
    216         private final ArraySet<String> mAllowedDataTypes = new ArraySet<>();
    217         private final Bundle mExtras = new Bundle();
    218         private CharSequence mLabel;
    219         private CharSequence[] mChoices;
    220         private int mFlags = DEFAULT_FLAGS;
    221         @EditChoicesBeforeSending
    222         private int mEditChoicesBeforeSending = EDIT_CHOICES_BEFORE_SENDING_AUTO;
    223 
    224         /**
    225          * Create a builder object for {@link RemoteInput} objects.
    226          *
    227          * @param resultKey the Bundle key that refers to this input when collected from the user
    228          */
    229         public Builder(@NonNull String resultKey) {
    230             if (resultKey == null) {
    231                 throw new IllegalArgumentException("Result key can't be null");
    232             }
    233             mResultKey = resultKey;
    234         }
    235 
    236         /**
    237          * Set a label to be displayed to the user when collecting this input.
    238          *
    239          * @param label The label to show to users when they input a response
    240          * @return this object for method chaining
    241          */
    242         @NonNull
    243         public Builder setLabel(@Nullable CharSequence label) {
    244             mLabel = Notification.safeCharSequence(label);
    245             return this;
    246         }
    247 
    248         /**
    249          * Specifies choices available to the user to satisfy this input.
    250          *
    251          * <p>Note: Starting in Android P, these choices will always be shown on phones if the app's
    252          * target SDK is >= P. However, these choices may also be rendered on other types of devices
    253          * regardless of target SDK.
    254          *
    255          * @param choices an array of pre-defined choices for users input.
    256          *        You must provide a non-null and non-empty array if
    257          *        you disabled free form input using {@link #setAllowFreeFormInput}
    258          * @return this object for method chaining
    259          */
    260         @NonNull
    261         public Builder setChoices(@Nullable CharSequence[] choices) {
    262             if (choices == null) {
    263                 mChoices = null;
    264             } else {
    265                 mChoices = new CharSequence[choices.length];
    266                 for (int i = 0; i < choices.length; i++) {
    267                     mChoices[i] = Notification.safeCharSequence(choices[i]);
    268                 }
    269             }
    270             return this;
    271         }
    272 
    273         /**
    274          * Specifies whether the user can provide arbitrary values. This allows an input
    275          * to accept non-textual values. Examples of usage are an input that wants audio
    276          * or an image.
    277          *
    278          * @param mimeType A mime type that results are allowed to come in.
    279          *         Be aware that text results (see {@link #setAllowFreeFormInput}
    280          *         are allowed by default. If you do not want text results you will have to
    281          *         pass false to {@code setAllowFreeFormInput}
    282          * @param doAllow Whether the mime type should be allowed or not
    283          * @return this object for method chaining
    284          */
    285         @NonNull
    286         public Builder setAllowDataType(@NonNull String mimeType, boolean doAllow) {
    287             if (doAllow) {
    288                 mAllowedDataTypes.add(mimeType);
    289             } else {
    290                 mAllowedDataTypes.remove(mimeType);
    291             }
    292             return this;
    293         }
    294 
    295         /**
    296          * Specifies whether the user can provide arbitrary text values.
    297          *
    298          * @param allowFreeFormTextInput The default is {@code true}.
    299          *         If you specify {@code false}, you must either provide a non-null
    300          *         and non-empty array to {@link #setChoices}, or enable a data result
    301          *         in {@code setAllowDataType}. Otherwise an
    302          *         {@link IllegalArgumentException} is thrown
    303          * @return this object for method chaining
    304          */
    305         @NonNull
    306         public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
    307             setFlag(FLAG_ALLOW_FREE_FORM_INPUT, allowFreeFormTextInput);
    308             return this;
    309         }
    310 
    311         /**
    312          * Specifies whether tapping on a choice should let the user edit the input before it is
    313          * sent to the app. The default is {@link #EDIT_CHOICES_BEFORE_SENDING_AUTO}.
    314          *
    315          * It cannot be used if {@link #setAllowFreeFormInput} has been set to false.
    316          */
    317         @NonNull
    318         public Builder setEditChoicesBeforeSending(
    319                 @EditChoicesBeforeSending int editChoicesBeforeSending) {
    320             mEditChoicesBeforeSending = editChoicesBeforeSending;
    321             return this;
    322         }
    323 
    324         /**
    325          * Merge additional metadata into this builder.
    326          *
    327          * <p>Values within the Bundle will replace existing extras values in this Builder.
    328          *
    329          * @see RemoteInput#getExtras
    330          */
    331         @NonNull
    332         public Builder addExtras(@NonNull Bundle extras) {
    333             if (extras != null) {
    334                 mExtras.putAll(extras);
    335             }
    336             return this;
    337         }
    338 
    339         /**
    340          * Get the metadata Bundle used by this Builder.
    341          *
    342          * <p>The returned Bundle is shared with this Builder.
    343          */
    344         @NonNull
    345         public Bundle getExtras() {
    346             return mExtras;
    347         }
    348 
    349         private void setFlag(int mask, boolean value) {
    350             if (value) {
    351                 mFlags |= mask;
    352             } else {
    353                 mFlags &= ~mask;
    354             }
    355         }
    356 
    357         /**
    358          * Combine all of the options that have been set and return a new {@link RemoteInput}
    359          * object.
    360          */
    361         @NonNull
    362         public RemoteInput build() {
    363             return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mEditChoicesBeforeSending,
    364                     mExtras, mAllowedDataTypes);
    365         }
    366     }
    367 
    368     private RemoteInput(Parcel in) {
    369         mResultKey = in.readString();
    370         mLabel = in.readCharSequence();
    371         mChoices = in.readCharSequenceArray();
    372         mFlags = in.readInt();
    373         mEditChoicesBeforeSending = in.readInt();
    374         mExtras = in.readBundle();
    375         mAllowedDataTypes = (ArraySet<String>) in.readArraySet(null);
    376     }
    377 
    378     /**
    379      * Similar as {@link #getResultsFromIntent} but retrieves data results for a
    380      * specific RemoteInput result. To retrieve a value use:
    381      * <pre>
    382      * {@code
    383      * Map<String, Uri> results =
    384      *     RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
    385      * if (results != null) {
    386      *   Uri data = results.get(MIME_TYPE_OF_INTEREST);
    387      * }
    388      * }
    389      * </pre>
    390      * @param intent The intent object that fired in response to an action or content intent
    391      *               which also had one or more remote input requested.
    392      * @param remoteInputResultKey The result key for the RemoteInput you want results for.
    393      */
    394     public static Map<String, Uri> getDataResultsFromIntent(
    395             Intent intent, String remoteInputResultKey) {
    396         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    397         if (clipDataIntent == null) {
    398             return null;
    399         }
    400         Map<String, Uri> results = new HashMap<>();
    401         Bundle extras = clipDataIntent.getExtras();
    402         for (String key : extras.keySet()) {
    403           if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
    404               String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
    405               if (mimeType == null || mimeType.isEmpty()) {
    406                   continue;
    407               }
    408               Bundle bundle = clipDataIntent.getBundleExtra(key);
    409               String uriStr = bundle.getString(remoteInputResultKey);
    410               if (uriStr == null || uriStr.isEmpty()) {
    411                   continue;
    412               }
    413               results.put(mimeType, Uri.parse(uriStr));
    414           }
    415         }
    416         return results.isEmpty() ? null : results;
    417     }
    418 
    419     /**
    420      * Get the remote input text results bundle from an intent. The returned Bundle will
    421      * contain a key/value for every result key populated with text by remote input collector.
    422      * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For non-text
    423      * results use {@link #getDataResultsFromIntent}.
    424      * @param intent The intent object that fired in response to an action or content intent
    425      *               which also had one or more remote input requested.
    426      */
    427     public static Bundle getResultsFromIntent(Intent intent) {
    428         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    429         if (clipDataIntent == null) {
    430             return null;
    431         }
    432         return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
    433     }
    434 
    435     /**
    436      * Populate an intent object with the text results gathered from remote input. This method
    437      * should only be called by remote input collection services when sending results to a
    438      * pending intent.
    439      * @param remoteInputs The remote inputs for which results are being provided
    440      * @param intent The intent to add remote inputs to. The {@link ClipData}
    441      *               field of the intent will be modified to contain the results.
    442      * @param results A bundle holding the remote input results. This bundle should
    443      *                be populated with keys matching the result keys specified in
    444      *                {@code remoteInputs} with values being the CharSequence results per key.
    445      */
    446     public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
    447             Bundle results) {
    448         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    449         if (clipDataIntent == null) {
    450             clipDataIntent = new Intent();  // First time we've added a result.
    451         }
    452         Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA);
    453         if (resultsBundle == null) {
    454             resultsBundle = new Bundle();
    455         }
    456         for (RemoteInput remoteInput : remoteInputs) {
    457             Object result = results.get(remoteInput.getResultKey());
    458             if (result instanceof CharSequence) {
    459                 resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
    460             }
    461         }
    462         clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
    463         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    464     }
    465 
    466     /**
    467      * Same as {@link #addResultsToIntent} but for setting data results. This is used
    468      * for inputs that accept non-textual results (see {@link Builder#setAllowDataType}).
    469      * Only one result can be provided for every mime type accepted by the RemoteInput.
    470      * If multiple inputs of the same mime type are expected then multiple RemoteInputs
    471      * should be used.
    472      *
    473      * @param remoteInput The remote input for which results are being provided
    474      * @param intent The intent to add remote input results to. The {@link ClipData}
    475      *               field of the intent will be modified to contain the results.
    476      * @param results A map of mime type to the Uri result for that mime type.
    477      */
    478     public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
    479             Map<String, Uri> results) {
    480         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    481         if (clipDataIntent == null) {
    482             clipDataIntent = new Intent();  // First time we've added a result.
    483         }
    484         for (Map.Entry<String, Uri> entry : results.entrySet()) {
    485             String mimeType = entry.getKey();
    486             Uri uri = entry.getValue();
    487             if (mimeType == null) {
    488                 continue;
    489             }
    490             Bundle resultsBundle =
    491                     clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
    492             if (resultsBundle == null) {
    493                 resultsBundle = new Bundle();
    494             }
    495             resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
    496 
    497             clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
    498         }
    499         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    500     }
    501 
    502     /**
    503      * Set the source of the RemoteInput results. This method should only be called by remote
    504      * input collection services (e.g.
    505      * {@link android.service.notification.NotificationListenerService})
    506      * when sending results to a pending intent.
    507      *
    508      * @see #SOURCE_FREE_FORM_INPUT
    509      * @see #SOURCE_CHOICE
    510      *
    511      * @param intent The intent to add remote input source to. The {@link ClipData}
    512      *               field of the intent will be modified to contain the source.
    513      * @param source The source of the results.
    514      */
    515     public static void setResultsSource(Intent intent, @Source int source) {
    516         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    517         if (clipDataIntent == null) {
    518             clipDataIntent = new Intent();  // First time we've added a result.
    519         }
    520         clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
    521         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    522     }
    523 
    524     /**
    525      * Get the source of the RemoteInput results.
    526      *
    527      * @see #SOURCE_FREE_FORM_INPUT
    528      * @see #SOURCE_CHOICE
    529      *
    530      * @param intent The intent object that fired in response to an action or content intent
    531      *               which also had one or more remote input requested.
    532      * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
    533      * be returned.
    534      */
    535     @Source
    536     public static int getResultsSource(Intent intent) {
    537         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    538         if (clipDataIntent == null) {
    539             return SOURCE_FREE_FORM_INPUT;
    540         }
    541         return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
    542     }
    543 
    544     private static String getExtraResultsKeyForData(String mimeType) {
    545         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
    546     }
    547 
    548     @Override
    549     public int describeContents() {
    550         return 0;
    551     }
    552 
    553     @Override
    554     public void writeToParcel(Parcel out, int flags) {
    555         out.writeString(mResultKey);
    556         out.writeCharSequence(mLabel);
    557         out.writeCharSequenceArray(mChoices);
    558         out.writeInt(mFlags);
    559         out.writeInt(mEditChoicesBeforeSending);
    560         out.writeBundle(mExtras);
    561         out.writeArraySet(mAllowedDataTypes);
    562     }
    563 
    564     public static final @android.annotation.NonNull Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() {
    565         @Override
    566         public RemoteInput createFromParcel(Parcel in) {
    567             return new RemoteInput(in);
    568         }
    569 
    570         @Override
    571         public RemoteInput[] newArray(int size) {
    572             return new RemoteInput[size];
    573         }
    574     };
    575 
    576     private static Intent getClipDataIntentFromIntent(Intent intent) {
    577         ClipData clipData = intent.getClipData();
    578         if (clipData == null) {
    579             return null;
    580         }
    581         ClipDescription clipDescription = clipData.getDescription();
    582         if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
    583             return null;
    584         }
    585         if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
    586             return null;
    587         }
    588         return clipData.getItemAt(0).getIntent();
    589     }
    590 }
    591