Home | History | Annotate | Download | only in mbms
      1 /*
      2  * Copyright (C) 2017 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.telephony.mbms;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.SystemApi;
     21 import android.annotation.TestApi;
     22 import android.content.Intent;
     23 import android.net.Uri;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.util.Base64;
     27 import android.util.Log;
     28 
     29 import java.io.ByteArrayInputStream;
     30 import java.io.ByteArrayOutputStream;
     31 import java.io.Externalizable;
     32 import java.io.File;
     33 import java.io.IOException;
     34 import java.io.ObjectInput;
     35 import java.io.ObjectInputStream;
     36 import java.io.ObjectOutput;
     37 import java.io.ObjectOutputStream;
     38 import java.net.URISyntaxException;
     39 import java.nio.charset.StandardCharsets;
     40 import java.security.MessageDigest;
     41 import java.security.NoSuchAlgorithmException;
     42 import java.util.Objects;
     43 
     44 /**
     45  * Describes a request to download files over cell-broadcast. Instances of this class should be
     46  * created by the app when requesting a download, and instances of this class will be passed back
     47  * to the app when the middleware updates the status of the download.
     48  */
     49 public final class DownloadRequest implements Parcelable {
     50     // Version code used to keep token calculation consistent.
     51     private static final int CURRENT_VERSION = 1;
     52     private static final String LOG_TAG = "MbmsDownloadRequest";
     53 
     54     /** @hide */
     55     public static final int MAX_APP_INTENT_SIZE = 50000;
     56 
     57     /** @hide */
     58     public static final int MAX_DESTINATION_URI_SIZE = 50000;
     59 
     60     /** @hide */
     61     private static class SerializationDataContainer implements Externalizable {
     62         private String fileServiceId;
     63         private Uri source;
     64         private Uri destination;
     65         private int subscriptionId;
     66         private String appIntent;
     67         private int version;
     68 
     69         public SerializationDataContainer() {}
     70 
     71         SerializationDataContainer(DownloadRequest request) {
     72             fileServiceId = request.fileServiceId;
     73             source = request.sourceUri;
     74             destination = request.destinationUri;
     75             subscriptionId = request.subscriptionId;
     76             appIntent = request.serializedResultIntentForApp;
     77             version = request.version;
     78         }
     79 
     80         @Override
     81         public void writeExternal(ObjectOutput objectOutput) throws IOException {
     82             objectOutput.write(version);
     83             objectOutput.writeUTF(fileServiceId);
     84             objectOutput.writeUTF(source.toString());
     85             objectOutput.writeUTF(destination.toString());
     86             objectOutput.write(subscriptionId);
     87             objectOutput.writeUTF(appIntent);
     88         }
     89 
     90         @Override
     91         public void readExternal(ObjectInput objectInput) throws IOException {
     92             version = objectInput.read();
     93             fileServiceId = objectInput.readUTF();
     94             source = Uri.parse(objectInput.readUTF());
     95             destination = Uri.parse(objectInput.readUTF());
     96             subscriptionId = objectInput.read();
     97             appIntent = objectInput.readUTF();
     98             // Do version checks here -- future versions may have other fields.
     99         }
    100     }
    101 
    102     public static class Builder {
    103         private String fileServiceId;
    104         private Uri source;
    105         private Uri destination;
    106         private int subscriptionId;
    107         private String appIntent;
    108         private int version = CURRENT_VERSION;
    109 
    110         /**
    111          * Constructs a {@link Builder} from a {@link DownloadRequest}
    112          * @param other The {@link DownloadRequest} from which the data for the {@link Builder}
    113          *              should come.
    114          * @return An instance of {@link Builder} pre-populated with data from the provided
    115          *         {@link DownloadRequest}.
    116          */
    117         public static Builder fromDownloadRequest(DownloadRequest other) {
    118             Builder result = new Builder(other.sourceUri, other.destinationUri)
    119                     .setServiceId(other.fileServiceId)
    120                     .setSubscriptionId(other.subscriptionId);
    121             result.appIntent = other.serializedResultIntentForApp;
    122             // Version of the result is going to be the current version -- as this class gets
    123             // updated, new fields will be set to default values in here.
    124             return result;
    125         }
    126 
    127         /**
    128          * This method constructs a new instance of {@link Builder} based on the serialized data
    129          * passed in.
    130          * @param data A byte array, the contents of which should have been originally obtained
    131          *             from {@link DownloadRequest#toByteArray()}.
    132          */
    133         public static Builder fromSerializedRequest(byte[] data) {
    134             Builder builder;
    135             try {
    136                 ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
    137                 SerializationDataContainer dataContainer =
    138                         (SerializationDataContainer) stream.readObject();
    139                 builder = new Builder(dataContainer.source, dataContainer.destination);
    140                 builder.version = dataContainer.version;
    141                 builder.appIntent = dataContainer.appIntent;
    142                 builder.fileServiceId = dataContainer.fileServiceId;
    143                 builder.subscriptionId = dataContainer.subscriptionId;
    144             } catch (IOException e) {
    145                 // Really should never happen
    146                 Log.e(LOG_TAG, "Got IOException trying to parse opaque data");
    147                 throw new IllegalArgumentException(e);
    148             } catch (ClassNotFoundException e) {
    149                 Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data");
    150                 throw new IllegalArgumentException(e);
    151             }
    152             return builder;
    153         }
    154 
    155         /**
    156          * Builds a new DownloadRequest.
    157          * @param sourceUri the source URI for the DownloadRequest to be built. This URI should
    158          *     never be null.
    159          * @param destinationUri The final location for the file(s) that are to be downloaded. It
    160          *     must be on the same filesystem as the temp file directory set via
    161          *     {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}.
    162          *     The provided path must be a directory that exists. An
    163          *     {@link IllegalArgumentException} will be thrown otherwise.
    164          */
    165         public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) {
    166             if (sourceUri == null || destinationUri == null) {
    167                 throw new IllegalArgumentException("Source and destination URIs must be non-null.");
    168             }
    169             source = sourceUri;
    170             destination = destinationUri;
    171         }
    172 
    173         /**
    174          * Sets the service from which the download request to be built will download from.
    175          * @param serviceInfo
    176          * @return
    177          */
    178         public Builder setServiceInfo(FileServiceInfo serviceInfo) {
    179             fileServiceId = serviceInfo.getServiceId();
    180             return this;
    181         }
    182 
    183         /**
    184          * Set the service ID for the download request. For use by the middleware only.
    185          * @hide
    186          */
    187         @SystemApi
    188         @TestApi
    189         public Builder setServiceId(String serviceId) {
    190             fileServiceId = serviceId;
    191             return this;
    192         }
    193 
    194         /**
    195          * Set the subscription ID on which the file(s) should be downloaded.
    196          * @param subscriptionId
    197          */
    198         public Builder setSubscriptionId(int subscriptionId) {
    199             this.subscriptionId = subscriptionId;
    200             return this;
    201         }
    202 
    203         /**
    204          * Set the {@link Intent} that should be sent when the download completes or fails. This
    205          * should be an intent with a explicit {@link android.content.ComponentName} targeted to a
    206          * {@link android.content.BroadcastReceiver} in the app's package.
    207          *
    208          * The middleware should not use this method.
    209          * @param intent
    210          */
    211         public Builder setAppIntent(Intent intent) {
    212             this.appIntent = intent.toUri(0);
    213             if (this.appIntent.length() > MAX_APP_INTENT_SIZE) {
    214                 throw new IllegalArgumentException("App intent must not exceed length " +
    215                         MAX_APP_INTENT_SIZE);
    216             }
    217             return this;
    218         }
    219 
    220         public DownloadRequest build() {
    221             return new DownloadRequest(fileServiceId, source, destination,
    222                     subscriptionId, appIntent, version);
    223         }
    224     }
    225 
    226     private final String fileServiceId;
    227     private final Uri sourceUri;
    228     private final Uri destinationUri;
    229     private final int subscriptionId;
    230     private final String serializedResultIntentForApp;
    231     private final int version;
    232 
    233     private DownloadRequest(String fileServiceId,
    234             Uri source, Uri destination, int sub,
    235             String appIntent, int version) {
    236         this.fileServiceId = fileServiceId;
    237         sourceUri = source;
    238         subscriptionId = sub;
    239         destinationUri = destination;
    240         serializedResultIntentForApp = appIntent;
    241         this.version = version;
    242     }
    243 
    244     private DownloadRequest(Parcel in) {
    245         fileServiceId = in.readString();
    246         sourceUri = in.readParcelable(getClass().getClassLoader());
    247         destinationUri = in.readParcelable(getClass().getClassLoader());
    248         subscriptionId = in.readInt();
    249         serializedResultIntentForApp = in.readString();
    250         version = in.readInt();
    251     }
    252 
    253     public int describeContents() {
    254         return 0;
    255     }
    256 
    257     public void writeToParcel(Parcel out, int flags) {
    258         out.writeString(fileServiceId);
    259         out.writeParcelable(sourceUri, flags);
    260         out.writeParcelable(destinationUri, flags);
    261         out.writeInt(subscriptionId);
    262         out.writeString(serializedResultIntentForApp);
    263         out.writeInt(version);
    264     }
    265 
    266     /**
    267      * @return The ID of the file service to download from.
    268      */
    269     public String getFileServiceId() {
    270         return fileServiceId;
    271     }
    272 
    273     /**
    274      * @return The source URI to download from
    275      */
    276     public Uri getSourceUri() {
    277         return sourceUri;
    278     }
    279 
    280     /**
    281      * @return The destination {@link Uri} of the downloaded file.
    282      */
    283     public Uri getDestinationUri() {
    284         return destinationUri;
    285     }
    286 
    287     /**
    288      * @return The subscription ID on which to perform MBMS operations.
    289      */
    290     public int getSubscriptionId() {
    291         return subscriptionId;
    292     }
    293 
    294     /**
    295      * For internal use -- returns the intent to send to the app after download completion or
    296      * failure.
    297      * @hide
    298      */
    299     public Intent getIntentForApp() {
    300         try {
    301             return Intent.parseUri(serializedResultIntentForApp, 0);
    302         } catch (URISyntaxException e) {
    303             return null;
    304         }
    305     }
    306 
    307     /**
    308      * This method returns a byte array that may be persisted to disk and restored to a
    309      * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
    310      * may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
    311      * @return A byte array of data to persist.
    312      */
    313     public byte[] toByteArray() {
    314         try {
    315             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    316             ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
    317             SerializationDataContainer container = new SerializationDataContainer(this);
    318             stream.writeObject(container);
    319             stream.flush();
    320             return byteArrayOutputStream.toByteArray();
    321         } catch (IOException e) {
    322             // Really should never happen
    323             Log.e(LOG_TAG, "Got IOException trying to serialize opaque data");
    324             return null;
    325         }
    326     }
    327 
    328     /** @hide */
    329     public int getVersion() {
    330         return version;
    331     }
    332 
    333     public static final Parcelable.Creator<DownloadRequest> CREATOR =
    334             new Parcelable.Creator<DownloadRequest>() {
    335         public DownloadRequest createFromParcel(Parcel in) {
    336             return new DownloadRequest(in);
    337         }
    338         public DownloadRequest[] newArray(int size) {
    339             return new DownloadRequest[size];
    340         }
    341     };
    342 
    343     /**
    344      * Maximum permissible length for the app's destination path, when serialized via
    345      * {@link Uri#toString()}.
    346      */
    347     public static int getMaxAppIntentSize() {
    348         return MAX_APP_INTENT_SIZE;
    349     }
    350 
    351     /**
    352      * Maximum permissible length for the app's download-completion intent, when serialized via
    353      * {@link Intent#toUri(int)}.
    354      */
    355     public static int getMaxDestinationUriSize() {
    356         return MAX_DESTINATION_URI_SIZE;
    357     }
    358 
    359     /**
    360      * Retrieves the hash string that should be used as the filename when storing a token for
    361      * this DownloadRequest.
    362      * @hide
    363      */
    364     public String getHash() {
    365         MessageDigest digest;
    366         try {
    367             digest = MessageDigest.getInstance("SHA-256");
    368         } catch (NoSuchAlgorithmException e) {
    369             throw new RuntimeException("Could not get sha256 hash object");
    370         }
    371         if (version >= 1) {
    372             // Hash the source, destination, and the app intent
    373             digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
    374             digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
    375             if (serializedResultIntentForApp != null) {
    376                 digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
    377             }
    378         }
    379         // Add updates for future versions here
    380         return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP);
    381     }
    382 
    383     @Override
    384     public boolean equals(Object o) {
    385         if (this == o) return true;
    386         if (o == null) {
    387             return false;
    388         }
    389         if (!(o instanceof DownloadRequest)) {
    390             return false;
    391         }
    392         DownloadRequest request = (DownloadRequest) o;
    393         return subscriptionId == request.subscriptionId &&
    394                 version == request.version &&
    395                 Objects.equals(fileServiceId, request.fileServiceId) &&
    396                 Objects.equals(sourceUri, request.sourceUri) &&
    397                 Objects.equals(destinationUri, request.destinationUri) &&
    398                 Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
    399     }
    400 
    401     @Override
    402     public int hashCode() {
    403         return Objects.hash(fileServiceId, sourceUri, destinationUri,
    404                 subscriptionId, serializedResultIntentForApp, version);
    405     }
    406 }
    407