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