content: /*
   * Copyright (C) 2014 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.app;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  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.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  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
   * an intent inside a {@link android.app.PendingIntent} that is sent.
   * Always use {@link RemoteInput.Builder} to create instances of this class.
   * <p class="note"> See
   * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html#direct">Replying
   * to notifications</a> for more information on how to use this class.
   *
   * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action},
   * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}.
   * Users are prompted to input a response when they trigger the action. The results are sent along
   * with the intent and can be retrieved with the result key (provided to the {@link Builder}
   * constructor) from the Bundle returned by {@link #getResultsFromIntent}.
   *
   * <pre class="prettyprint">
   * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
   * Notification.Action action = new Notification.Action.Builder(
   *         R.drawable.reply, &quot;Reply&quot;, actionIntent)
   *         <b>.addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
   *                 .setLabel("Quick reply").build()</b>)
   *         .build();</pre>
   *
   * <p>When the {@link android.app.PendingIntent} is fired, the intent inside will contain the
   * input results if collected. To access these results, use the {@link #getResultsFromIntent}
   * function. The result values will present under the result key passed to the {@link Builder}
   * constructor.
   *
   * <pre class="prettyprint">
   * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
   * Bundle results = RemoteInput.getResultsFromIntent(intent);
   * if (results != null) {
   *     CharSequence quickReplyResult = results.getCharSequence(KEY_QUICK_REPLY_TEXT);
   * }</pre>
   */
  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 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";
  
      /** Extra added to a clip data intent object identifying the {@link Source} of the results. */
      private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
  
      /** @hide */
      @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_FREE_FORM_INPUT, SOURCE_CHOICE})
      @Retention(RetentionPolicy.SOURCE)
      public @interface Source {}
  
      /** The user manually entered the data. */
      public static final int SOURCE_FREE_FORM_INPUT = 0;
  
      /** The user selected one of the choices from {@link #getChoices}. */
      public static final int SOURCE_CHOICE = 1;
  
      /** @hide */
      @IntDef(prefix = {"EDIT_CHOICES_BEFORE_SENDING_"},
              value = {EDIT_CHOICES_BEFORE_SENDING_AUTO, EDIT_CHOICES_BEFORE_SENDING_DISABLED,
                      EDIT_CHOICES_BEFORE_SENDING_ENABLED})
      @Retention(RetentionPolicy.SOURCE)
      public @interface EditChoicesBeforeSending {}
  
      /** The platform will determine whether choices will be edited before being sent to the app. */
      public static final int EDIT_CHOICES_BEFORE_SENDING_AUTO = 0;
  
      /** Tapping on a choice should send the input immediately, without letting the user edit it. */
      public static final int EDIT_CHOICES_BEFORE_SENDING_DISABLED = 1;
  
      /** Tapping on a choice should let the user edit the input before it is sent to the app. */
      public static final int EDIT_CHOICES_BEFORE_SENDING_ENABLED = 2;
  
      // Flags bitwise-ored to mFlags
      private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
  
      // Default value for flags integer
      private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT;
  
      private final String mResultKey;
      private final CharSequence mLabel;
      private final CharSequence[] mChoices;
      private final int mFlags;
      @EditChoicesBeforeSending private final int mEditChoicesBeforeSending;
      private final Bundle mExtras;
      private final ArraySet<String> mAllowedDataTypes;
  
      private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
              int flags, int editChoicesBeforeSending, Bundle extras,
              ArraySet<String> allowedDataTypes) {
          this.mResultKey = resultKey;
          this.mLabel = label;
          this.mChoices = choices;
          this.mFlags = flags;
          this.mEditChoicesBeforeSending = editChoicesBeforeSending;
          this.mExtras = extras;
          this.mAllowedDataTypes = allowedDataTypes;
          if (getEditChoicesBeforeSending() == EDIT_CHOICES_BEFORE_SENDING_ENABLED
                  && !getAllowFreeFormInput()) {
              throw new IllegalArgumentException(
                      "setEditChoicesBeforeSending requires setAllowFreeFormInput");
          }
      }
  
      /**
       * Get the key that the result of this input will be set in from the Bundle returned by
       * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
       */
      public String getResultKey() {
          return mResultKey;
      }
  
      /**
       * Get the label to display to users when collecting this input.
       */
      public CharSequence getLabel() {
          return mLabel;
      }
  
      /**
       * Get possible input choices. This can be {@code null} if there are no choices to present.
       */
      public CharSequence[] getChoices() {
          return mChoices;
      }
  
      /**
       * Get possible non-textual inputs that are accepted.
       * This can be {@code null} if the input does not accept non-textual values.
       * See {@link Builder#setAllowDataType}.
       */
      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
       * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
       * if you set this to false and {@link #getChoices} returns {@code null} or empty.
       */
      public boolean getAllowFreeFormInput() {
          return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0;
      }
  
      /**
       * Gets whether tapping on a choice should let the user edit the input before it is sent to the
       * app.
       */
      @EditChoicesBeforeSending
      public int getEditChoicesBeforeSending() {
          return mEditChoicesBeforeSending;
      }
  
      /**
       * Get additional metadata carried around with this remote input.
       */
      public Bundle getExtras() {
          return mExtras;
      }
  
      /**
       * Builder class for {@link RemoteInput} objects.
       */
      public static final class Builder {
          private final String mResultKey;
          private final ArraySet<String> mAllowedDataTypes = new ArraySet<>();
          private final Bundle mExtras = new Bundle();
          private CharSequence mLabel;
          private CharSequence[] mChoices;
          private int mFlags = DEFAULT_FLAGS;
          @EditChoicesBeforeSending
          private int mEditChoicesBeforeSending = EDIT_CHOICES_BEFORE_SENDING_AUTO;
  
          /**
           * Create a builder object for {@link RemoteInput} objects.
           *
           * @param resultKey the Bundle key that refers to this input when collected from the user
           */
          public Builder(@NonNull String resultKey) {
              if (resultKey == null) {
                  throw new IllegalArgumentException("Result key can't be null");
              }
              mResultKey = resultKey;
          }
  
          /**
           * Set a label to be displayed to the user when collecting this input.
           *
           * @param label The label to show to users when they input a response
           * @return this object for method chaining
           */
          @NonNull
          public Builder setLabel(@Nullable CharSequence label) {
              mLabel = Notification.safeCharSequence(label);
              return this;
          }
  
          /**
           * Specifies choices available to the user to satisfy this input.
           *
           * <p>Note: Starting in Android P, these choices will always be shown on phones if the app's
           * target SDK is >= P. However, these choices may also be rendered on other types of devices
           * regardless of target SDK.
           *
           * @param choices an array of pre-defined choices for users input.
           *        You must provide a non-null and non-empty array if
           *        you disabled free form input using {@link #setAllowFreeFormInput}
           * @return this object for method chaining
           */
          @NonNull
          public Builder setChoices(@Nullable CharSequence[] choices) {
              if (choices == null) {
                  mChoices = null;
              } else {
                  mChoices = new CharSequence[choices.length];
                  for (int i = 0; i < choices.length; i++) {
                      mChoices[i] = Notification.safeCharSequence(choices[i]);
                  }
              }
              return this;
          }
  
          /**
           * Specifies whether the user can provide arbitrary values. This allows an input
           * to accept non-textual values. Examples of usage are an input that wants audio
           * or an image.
           *
           * @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
           */
          @NonNull
          public Builder setAllowDataType(@NonNull 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
           */
          @NonNull
          public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
              setFlag(FLAG_ALLOW_FREE_FORM_INPUT, allowFreeFormTextInput);
              return this;
          }
  
          /**
           * Specifies whether tapping on a choice should let the user edit the input before it is
           * sent to the app. The default is {@link #EDIT_CHOICES_BEFORE_SENDING_AUTO}.
           *
           * It cannot be used if {@link #setAllowFreeFormInput} has been set to false.
           */
          @NonNull
          public Builder setEditChoicesBeforeSending(
                  @EditChoicesBeforeSending int editChoicesBeforeSending) {
              mEditChoicesBeforeSending = editChoicesBeforeSending;
              return this;
          }
  
          /**
           * Merge additional metadata into this builder.
           *
           * <p>Values within the Bundle will replace existing extras values in this Builder.
           *
           * @see RemoteInput#getExtras
           */
          @NonNull
          public Builder addExtras(@NonNull Bundle extras) {
              if (extras != null) {
                  mExtras.putAll(extras);
              }
              return this;
          }
  
          /**
           * Get the metadata Bundle used by this Builder.
           *
           * <p>The returned Bundle is shared with this Builder.
           */
          @NonNull
          public Bundle getExtras() {
              return mExtras;
          }
  
          private void setFlag(int mask, boolean value) {
              if (value) {
                  mFlags |= mask;
              } else {
                  mFlags &= ~mask;
              }
          }
  
          /**
           * Combine all of the options that have been set and return a new {@link RemoteInput}
           * object.
           */
          @NonNull
          public RemoteInput build() {
              return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mEditChoicesBeforeSending,
                      mExtras, mAllowedDataTypes);
          }
      }
  
      private RemoteInput(Parcel in) {
          mResultKey = in.readString();
          mLabel = in.readCharSequence();
          mChoices = in.readCharSequenceArray();
          mFlags = in.readInt();
          mEditChoicesBeforeSending = in.readInt();
          mExtras = in.readBundle();
          mAllowedDataTypes = (ArraySet<String>) in.readArraySet(null);
      }
  
      /**
       * 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) {
          Intent clipDataIntent = getClipDataIntentFromIntent(intent);
          if (clipDataIntent == null) {
              return null;
          }
          return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
      }
  
      /**
       * 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
       * @param intent The intent to add remote inputs to. The {@link ClipData}
       *               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 CharSequence results per key.
       */
      public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
              Bundle results) {
          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);
              }
          }
          clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
          intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
      }
  
      /**
       * Same as {@link #addResultsToIntent} but for setting data results. This is used
       * for inputs that accept non-textual results (see {@link Builder#setAllowDataType}).
       * Only one result can be provided for every mime type accepted by the RemoteInput.
       * If multiple inputs of the same mime type are expected then multiple RemoteInputs
       * should be used.
       *
       * @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));
      }
  
      /**
       * Set the source of the RemoteInput results. This method should only be called by remote
       * input collection services (e.g.
       * {@link android.service.notification.NotificationListenerService})
       * when sending results to a pending intent.
       *
       * @see #SOURCE_FREE_FORM_INPUT
       * @see #SOURCE_CHOICE
       *
       * @param intent The intent to add remote input source to. The {@link ClipData}
       *               field of the intent will be modified to contain the source.
       * @param source The source of the results.
       */
      public static void setResultsSource(Intent intent, @Source int source) {
          Intent clipDataIntent = getClipDataIntentFromIntent(intent);
          if (clipDataIntent == null) {
              clipDataIntent = new Intent();  // First time we've added a result.
          }
          clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
          intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
      }
  
      /**
       * Get the source of the RemoteInput results.
       *
       * @see #SOURCE_FREE_FORM_INPUT
       * @see #SOURCE_CHOICE
       *
       * @param intent The intent object that fired in response to an action or content intent
       *               which also had one or more remote input requested.
       * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
       * be returned.
       */
      @Source
      public static int getResultsSource(Intent intent) {
          Intent clipDataIntent = getClipDataIntentFromIntent(intent);
          if (clipDataIntent == null) {
              return SOURCE_FREE_FORM_INPUT;
          }
          return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
      }
  
      private static String getExtraResultsKeyForData(String mimeType) {
          return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      @Override
      public void writeToParcel(Parcel out, int flags) {
          out.writeString(mResultKey);
          out.writeCharSequence(mLabel);
          out.writeCharSequenceArray(mChoices);
          out.writeInt(mFlags);
          out.writeInt(mEditChoicesBeforeSending);
          out.writeBundle(mExtras);
          out.writeArraySet(mAllowedDataTypes);
      }
  
      public static final @android.annotation.NonNull Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() {
          @Override
          public RemoteInput createFromParcel(Parcel in) {
              return new RemoteInput(in);
          }
  
          @Override
          public RemoteInput[] newArray(int size) {
              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();
      }
  }
  