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
17package android.telephony.mbms;
18
19import android.annotation.NonNull;
20import android.annotation.SystemApi;
21import android.annotation.TestApi;
22import android.content.Intent;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.util.Base64;
27import android.util.Log;
28
29import java.io.ByteArrayInputStream;
30import java.io.ByteArrayOutputStream;
31import java.io.Externalizable;
32import java.io.File;
33import java.io.IOException;
34import java.io.ObjectInput;
35import java.io.ObjectInputStream;
36import java.io.ObjectOutput;
37import java.io.ObjectOutputStream;
38import java.net.URISyntaxException;
39import java.nio.charset.StandardCharsets;
40import java.security.MessageDigest;
41import java.security.NoSuchAlgorithmException;
42import 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 */
49public 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