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.Nullable;
21 import android.annotation.SystemApi;
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
setServiceId(String serviceId)188         public Builder setServiceId(String serviceId) {
189             fileServiceId = serviceId;
190             return this;
191         }
192 
193         /**
194          * Set the subscription ID on which the file(s) should be downloaded.
195          * @param subscriptionId
196          */
setSubscriptionId(int subscriptionId)197         public Builder setSubscriptionId(int subscriptionId) {
198             this.subscriptionId = subscriptionId;
199             return this;
200         }
201 
202         /**
203          * Set the {@link Intent} that should be sent when the download completes or fails. This
204          * should be an intent with a explicit {@link android.content.ComponentName} targeted to a
205          * {@link android.content.BroadcastReceiver} in the app's package.
206          *
207          * The middleware should not use this method.
208          * @param intent
209          */
setAppIntent(Intent intent)210         public Builder setAppIntent(Intent intent) {
211             this.appIntent = intent.toUri(0);
212             if (this.appIntent.length() > MAX_APP_INTENT_SIZE) {
213                 throw new IllegalArgumentException("App intent must not exceed length " +
214                         MAX_APP_INTENT_SIZE);
215             }
216             return this;
217         }
218 
build()219         public DownloadRequest build() {
220             return new DownloadRequest(fileServiceId, source, destination,
221                     subscriptionId, appIntent, version);
222         }
223     }
224 
225     private final String fileServiceId;
226     private final Uri sourceUri;
227     private final Uri destinationUri;
228     private final int subscriptionId;
229     private final String serializedResultIntentForApp;
230     private final int version;
231 
DownloadRequest(String fileServiceId, Uri source, Uri destination, int sub, String appIntent, int version)232     private DownloadRequest(String fileServiceId,
233             Uri source, Uri destination, int sub,
234             String appIntent, int version) {
235         this.fileServiceId = fileServiceId;
236         sourceUri = source;
237         subscriptionId = sub;
238         destinationUri = destination;
239         serializedResultIntentForApp = appIntent;
240         this.version = version;
241     }
242 
DownloadRequest(Parcel in)243     private DownloadRequest(Parcel in) {
244         fileServiceId = in.readString();
245         sourceUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class);
246         destinationUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class);
247         subscriptionId = in.readInt();
248         serializedResultIntentForApp = in.readString();
249         version = in.readInt();
250     }
251 
describeContents()252     public int describeContents() {
253         return 0;
254     }
255 
writeToParcel(Parcel out, int flags)256     public void writeToParcel(Parcel out, int flags) {
257         out.writeString(fileServiceId);
258         out.writeParcelable(sourceUri, flags);
259         out.writeParcelable(destinationUri, flags);
260         out.writeInt(subscriptionId);
261         out.writeString(serializedResultIntentForApp);
262         out.writeInt(version);
263     }
264 
265     /**
266      * @return The ID of the file service to download from.
267      */
getFileServiceId()268     public String getFileServiceId() {
269         return fileServiceId;
270     }
271 
272     /**
273      * @return The source URI to download from
274      */
getSourceUri()275     public Uri getSourceUri() {
276         return sourceUri;
277     }
278 
279     /**
280      * @return The destination {@link Uri} of the downloaded file.
281      */
getDestinationUri()282     public Uri getDestinationUri() {
283         return destinationUri;
284     }
285 
286     /**
287      * @return The subscription ID on which to perform MBMS operations.
288      */
getSubscriptionId()289     public int getSubscriptionId() {
290         return subscriptionId;
291     }
292 
293     /**
294      * For internal use -- returns the intent to send to the app after download completion or
295      * failure.
296      * @hide
297      */
getIntentForApp()298     public Intent getIntentForApp() {
299         try {
300             return Intent.parseUri(serializedResultIntentForApp, 0);
301         } catch (URISyntaxException e) {
302             return null;
303         }
304     }
305 
306     /**
307      * This method returns a byte array that may be persisted to disk and restored to a
308      * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method
309      * may be recovered via {@link Builder#fromSerializedRequest(byte[])}.
310      * @return A byte array of data to persist.
311      */
toByteArray()312     public byte[] toByteArray() {
313         try {
314             ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
315             ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
316             SerializationDataContainer container = new SerializationDataContainer(this);
317             stream.writeObject(container);
318             stream.flush();
319             return byteArrayOutputStream.toByteArray();
320         } catch (IOException e) {
321             // Really should never happen
322             Log.e(LOG_TAG, "Got IOException trying to serialize opaque data");
323             return null;
324         }
325     }
326 
327     /** @hide */
getVersion()328     public int getVersion() {
329         return version;
330     }
331 
332     public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR =
333             new Parcelable.Creator<DownloadRequest>() {
334         public DownloadRequest createFromParcel(Parcel in) {
335             return new DownloadRequest(in);
336         }
337         public DownloadRequest[] newArray(int size) {
338             return new DownloadRequest[size];
339         }
340     };
341 
342     /**
343      * Maximum permissible length for the app's destination path, when serialized via
344      * {@link Uri#toString()}.
345      */
getMaxAppIntentSize()346     public static int getMaxAppIntentSize() {
347         return MAX_APP_INTENT_SIZE;
348     }
349 
350     /**
351      * Maximum permissible length for the app's download-completion intent, when serialized via
352      * {@link Intent#toUri(int)}.
353      */
getMaxDestinationUriSize()354     public static int getMaxDestinationUriSize() {
355         return MAX_DESTINATION_URI_SIZE;
356     }
357 
358     /**
359      * Retrieves the hash string that should be used as the filename when storing a token for
360      * this DownloadRequest.
361      * @hide
362      */
getHash()363     public String getHash() {
364         MessageDigest digest;
365         try {
366             digest = MessageDigest.getInstance("SHA-256");
367         } catch (NoSuchAlgorithmException e) {
368             throw new RuntimeException("Could not get sha256 hash object");
369         }
370         if (version >= 1) {
371             // Hash the source, destination, and the app intent
372             digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
373             digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
374             if (serializedResultIntentForApp != null) {
375                 digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8));
376             }
377         }
378         // Add updates for future versions here
379         return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP);
380     }
381 
382     @Override
equals(@ullable Object o)383     public boolean equals(@Nullable Object o) {
384         if (this == o) return true;
385         if (o == null) {
386             return false;
387         }
388         if (!(o instanceof DownloadRequest)) {
389             return false;
390         }
391         DownloadRequest request = (DownloadRequest) o;
392         return subscriptionId == request.subscriptionId &&
393                 version == request.version &&
394                 Objects.equals(fileServiceId, request.fileServiceId) &&
395                 Objects.equals(sourceUri, request.sourceUri) &&
396                 Objects.equals(destinationUri, request.destinationUri) &&
397                 Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp);
398     }
399 
400     @Override
hashCode()401     public int hashCode() {
402         return Objects.hash(fileServiceId, sourceUri, destinationUri,
403                 subscriptionId, serializedResultIntentForApp, version);
404     }
405 }
406