content: /*
   * Copyright (C) 2017 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.telephony.mbms;
  
  import android.annotation.NonNull;
  import android.annotation.SystemApi;
  import android.annotation.TestApi;
  import android.content.Intent;
  import android.net.Uri;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.util.Base64;
  import android.util.Log;
  
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.Externalizable;
  import java.io.File;
  import java.io.IOException;
  import java.io.ObjectInput;
  import java.io.ObjectInputStream;
  import java.io.ObjectOutput;
  import java.io.ObjectOutputStream;
  import java.net.URISyntaxException;
  import java.nio.charset.StandardCharsets;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.Objects;
  
  /**
   * Describes a request to download files over cell-broadcast. Instances of this class should be
   * created by the app when requesting a download, and instances of this class will be passed back
   * to the app when the middleware updates the status of the download.
   */
  public final class DownloadRequest implements Parcelable {
      // Version code used to keep token calculation consistent.
      private static final int CURRENT_VERSION = 1;
      private static final String LOG_TAG = "MbmsDownloadRequest";
  
      /** @hide */
      public static final int MAX_APP_INTENT_SIZE = 50000;
  
      /** @hide */
      public static final int MAX_DESTINATION_URI_SIZE = 50000;
  
      /** @hide */
      private static class SerializationDataContainer implements Externalizable {
          private String fileServiceId;
          private Uri source;
          private Uri destination;
          private int subscriptionId;
          private String appIntent;
          private int version;
  
          public SerializationDataContainer() {}
  
          SerializationDataContainer(DownloadRequest request) {
              fileServiceId = request.fileServiceId;
              source = request.sourceUri;
              destination = request.destinationUri;
              subscriptionId = request.subscriptionId;
              appIntent = request.serializedResultIntentForApp;
              version = request.version;
          }
  
          @Override
          public void writeExternal(ObjectOutput objectOutput) throws IOException {
              objectOutput.write(version);
              objectOutput.writeUTF(fileServiceId);
              objectOutput.writeUTF(source.toString());
              objectOutput.writeUTF(destination.toString());
              objectOutput.write(subscriptionId);
              objectOutput.writeUTF(appIntent);
          }
  
          @Override
          public void readExternal(ObjectInput objectInput) throws IOException {
              version = objectInput.read();
              fileServiceId = objectInput.readUTF();
              source = Uri.parse(objectInput.readUTF());
              destination = Uri.parse(objectInput.readUTF());
              subscriptionId = objectInput.read();
              appIntent = objectInput.readUTF();
              // Do version checks here -- future versions may have other fields.
          }
      }
  
      public static class Builder {
          private String fileServiceId;
          private Uri source;
          private Uri destination;
          private int subscriptionId;
          private String appIntent;
          private int version = CURRENT_VERSION;
  
          /**
           * Constructs a {@link Builder} from a {@link DownloadRequest}
           * @param other The {@link DownloadRequest} from which the data for the {@link Builder}
           *              should come.
           * @return An instance of {@link Builder} pre-populated with data from the provided
           *         {@link DownloadRequest}.
           */
          public static Builder fromDownloadRequest(DownloadRequest other) {
              Builder result = new Builder(other.sourceUri, other.destinationUri)
                      .setServiceId(other.fileServiceId)
                      .setSubscriptionId(other.subscriptionId);
              result.appIntent = other.serializedResultIntentForApp;
              // Version of the result is going to be the current version -- as this class gets
              // updated, new fields will be set to default values in here.
              return result;
          }
  
          /**
           * This method constructs a new instance of {@link Builder} based on the serialized data
           * passed in.
           * @param data A byte array, the contents of which should have been originally obtained
           *             from {@link DownloadRequest#toByteArray()}.
           */
          public static Builder fromSerializedRequest(byte[] data) {
              Builder builder;
              try {
                  ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
                  SerializationDataContainer dataContainer =
                          (SerializationDataContainer) stream.readObject();
                  builder = new Builder(dataContainer.source, dataContainer.destination);
                  builder.version = dataContainer.version;
                  builder.appIntent = dataContainer.appIntent;
                  builder.fileServiceId = dataContainer.fileServiceId;
                  builder.subscriptionId = dataContainer.subscriptionId;
              } catch (IOException e) {
                  // Really should never happen
                  Log.e(LOG_TAG, "Got IOException trying to parse opaque data");
                  throw new IllegalArgumentException(e);
              } catch (ClassNotFoundException e) {
                  Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data");
                  throw new IllegalArgumentException(e);
              }
              return builder;
          }
  
          /**
           * Builds a new DownloadRequest.
           * @param sourceUri the source URI for the DownloadRequest to be built. This URI should
           *     never be null.
           * @param destinationUri The final location for the file(s) that are to be downloaded. It
           *     must be on the same filesystem as the temp file directory set via
           *     {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}.
           *     The provided path must be a directory that exists. An
           *     {@link IllegalArgumentException} will be thrown otherwise.
           */
          public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) {
              if (sourceUri == null || destinationUri == null) {
                  throw new IllegalArgumentException("Source and destination URIs must be non-null.");
              }
              source = sourceUri;
              destination = destinationUri;
          }
  
          /**
           * Sets the service from which the download request to be built will download from.
           * @param serviceInfo
           * @return
           */
          public Builder setServiceInfo(FileServiceInfo serviceInfo) {
              fileServiceId = serviceInfo.getServiceId();
              return this;
          }
  
          /**
           * Set the service ID for the download request. For use by the middleware only.
           * @hide
           */
          @SystemApi
          @TestApi
          public Builder setServiceId(String serviceId) {
              fileServiceId = serviceId;
              return this;
          }
  
          /**
           * Set the subscription ID on which the file(s) should be downloaded.
           * @param subscriptionId
           */
          public Builder setSubscriptionId(int subscriptionId) {
              this.subscriptionId = subscriptionId;
              return this;
          }
  
          /**
           * Set the {@link Intent} that should be sent when the download completes or fails. This
           * should be an intent with a explicit {@link android.content.ComponentName} targeted to a
           * {@link android.content.BroadcastReceiver} in the app's package.
           *
           * The middleware should not use this method.
           * @param intent
           */
          public Builder setAppIntent(Intent intent) {
              this.appIntent = intent.toUri(0);
              if (this.appIntent.length() > MAX_APP_INTENT_SIZE) {
                  throw new IllegalArgumentException("App intent must not exceed length " +
                          MAX_APP_INTENT_SIZE);
              }
              return this;
          }
  
          public DownloadRequest build() {
              return new DownloadRequest(fileServiceId, source, destination,
                      subscriptionId, appIntent, version);
          }
      }
  
      private final String fileServiceId;
      private final Uri sourceUri;
      private final Uri destinationUri;
      private final int subscriptionId;
      private final String serializedResultIntentForApp;
      private final int version;
  
      private DownloadRequest(String fileServiceId,
              Uri source, Uri destination, int sub,
              String appIntent, int version) {
          this.fileServiceId = fileServiceId;
          sourceUri = source;
          subscriptionId = sub;
          destinationUri = destination;
          serializedResultIntentForApp = appIntent;
          this.version = version;
      }
  
      private DownloadRequest(Parcel in) {
          fileServiceId = in.readString();
          sourceUri = in.readParcelable(getClass().getClassLoader());
          destinationUri = in.readParcelable(getClass().getClassLoader());
          subscriptionId = in.readInt();
          serializedResultIntentForApp = in.readString();
          version = in.readInt();
      }
  
      public int describeContents() {
          return 0;
      }
  
      public void writeToParcel(Parcel out, int flags) {
          out.writeString(fileServiceId);
          out.writeParcelable(sourceUri, flags);
          out.writeParcelable(destinationUri, flags);
          out.writeInt(subscriptionId);
          out.writeString(serializedResultIntentForApp);
          out.writeInt(version);
      }
  
      /**
       * @return The ID of the file service to download from.
       */
      public String getFileServiceId() {
          return fileServiceId;
      }
  
      /**
       * @return The source URI to download from
       */
      public Uri getSourceUri() {
          return sourceUri;
      }
  
      /**
       * @return The destination {@link Uri} of the downloaded file.
       */
      public Uri getDestinationUri() {
          return destinationUri;
      }
  
      /**
       * @return The subscription ID on which to perform MBMS operations.
       */
      public int getSubscriptionId() {
          return subscriptionId;
      }
  
      /**
       * For internal use -- returns the intent to send to the app after download completion or
       * failure.
       * @hide
       */
      public Intent getIntentForApp() {
          try {
              return Intent.parseUri(serializedResultIntentForApp, 0);
          } catch (URISyntaxException e) {
              return null;
          }
      }
  
      /**
       * This method returns a byte array that may be persisted to disk and restored to a
       * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
       * may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
       * @return A byte array of data to persist.
       */
      public byte[] toByteArray() {
          try {
              ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
              ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
              SerializationDataContainer container = new SerializationDataContainer(this);
              stream.writeObject(container);
              stream.flush();
              return byteArrayOutputStream.toByteArray();
          } catch (IOException e) {
              // Really should never happen
              Log.e(LOG_TAG, "Got IOException trying to serialize opaque data");
              return null;
          }
      }
  
      /** @hide */
      public int getVersion() {
          return version;
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR =
              new Parcelable.Creator<DownloadRequest>() {
          public DownloadRequest createFromParcel(Parcel in) {
              return new DownloadRequest(in);
          }
          public DownloadRequest[] newArray(int size) {
              return new DownloadRequest[size];
          }
      };
  
      /**
       * Maximum permissible length for the app's destination path, when serialized via
       * {@link Uri#toString()}.
       */
      public static int getMaxAppIntentSize() {
          return MAX_APP_INTENT_SIZE;
      }
  
      /**
       * Maximum permissible length for the app's download-completion intent, when serialized via
       * {@link Intent#toUri(int)}.
       */
      public static int getMaxDestinationUriSize() {
          return MAX_DESTINATION_URI_SIZE;
      }
  
      /**
       * Retrieves the hash string that should be used as the filename when storing a token for
       * this DownloadRequest.
       * @hide
       */
      public String getHash() {
          MessageDigest digest;
          try {
              digest = MessageDigest.getInstance("SHA-256");
          } catch (NoSuchAlgorithmException e) {
              throw new RuntimeException("Could not get sha256 hash object");
          }
          if (version >= 1) {
              // Hash the source, destination, and the app intent
              digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
              digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
              if (serializedResultIntentForApp != null) {
                  digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
              }
          }
          // Add updates for future versions here
          return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP);
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null) {
              return false;
          }
          if (!(o instanceof DownloadRequest)) {
              return false;
          }
          DownloadRequest request = (DownloadRequest) o;
          return subscriptionId == request.subscriptionId &&
                  version == request.version &&
                  Objects.equals(fileServiceId, request.fileServiceId) &&
                  Objects.equals(sourceUri, request.sourceUri) &&
                  Objects.equals(destinationUri, request.destinationUri) &&
                  Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(fileServiceId, sourceUri, destinationUri,
                  subscriptionId, serializedResultIntentForApp, version);
      }
  }
  