• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SerializationDataContainer()69         public SerializationDataContainer() {}
70 
SerializationDataContainer(DownloadRequest request)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
writeExternal(ObjectOutput objectOutput)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
readExternal(ObjectInput objectInput)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          */
fromDownloadRequest(DownloadRequest other)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          */
fromSerializedRequest(byte[] data)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          */
Builder(@onNull Uri sourceUri, @NonNull Uri destinationUri)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          */
setServiceInfo(FileServiceInfo serviceInfo)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
setServiceId(String serviceId)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          */
setSubscriptionId(int subscriptionId)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          */
setAppIntent(Intent intent)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 
build()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 
DownloadRequest(String fileServiceId, Uri source, Uri destination, int sub, String appIntent, int version)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 
DownloadRequest(Parcel in)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 
describeContents()253     public int describeContents() {
254         return 0;
255     }
256 
writeToParcel(Parcel out, int flags)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      */
getFileServiceId()269     public String getFileServiceId() {
270         return fileServiceId;
271     }
272 
273     /**
274      * @return The source URI to download from
275      */
getSourceUri()276     public Uri getSourceUri() {
277         return sourceUri;
278     }
279 
280     /**
281      * @return The destination {@link Uri} of the downloaded file.
282      */
getDestinationUri()283     public Uri getDestinationUri() {
284         return destinationUri;
285     }
286 
287     /**
288      * @return The subscription ID on which to perform MBMS operations.
289      */
getSubscriptionId()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      */
getIntentForApp()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      */
toByteArray()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 */
getVersion()329     public int getVersion() {
330         return version;
331     }
332 
333     public static final @android.annotation.NonNull 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      */
getMaxAppIntentSize()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      */
getMaxDestinationUriSize()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      */
getHash()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
equals(Object o)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
hashCode()402     public int hashCode() {
403         return Objects.hash(fileServiceId, sourceUri, destinationUri,
404                 subscriptionId, serializedResultIntentForApp, version);
405     }
406 }
407