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