| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.telephony.mbms; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SystemApi; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.util.Base64; |
| import android.util.Log; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.Externalizable; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.ObjectInput; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutput; |
| import java.io.ObjectOutputStream; |
| import java.net.URISyntaxException; |
| import java.nio.charset.StandardCharsets; |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Objects; |
| |
| /** |
| * Describes a request to download files over cell-broadcast. Instances of this class should be |
| * created by the app when requesting a download, and instances of this class will be passed back |
| * to the app when the middleware updates the status of the download. |
| */ |
| public final class DownloadRequest implements Parcelable { |
| // Version code used to keep token calculation consistent. |
| private static final int CURRENT_VERSION = 1; |
| private static final String LOG_TAG = "MbmsDownloadRequest"; |
| |
| /** @hide */ |
| public static final int MAX_APP_INTENT_SIZE = 50000; |
| |
| /** @hide */ |
| public static final int MAX_DESTINATION_URI_SIZE = 50000; |
| |
| /** @hide */ |
| private static class SerializationDataContainer implements Externalizable { |
| private String fileServiceId; |
| private Uri source; |
| private Uri destination; |
| private int subscriptionId; |
| private String appIntent; |
| private int version; |
| |
| public SerializationDataContainer() {} |
| |
| SerializationDataContainer(DownloadRequest request) { |
| fileServiceId = request.fileServiceId; |
| source = request.sourceUri; |
| destination = request.destinationUri; |
| subscriptionId = request.subscriptionId; |
| appIntent = request.serializedResultIntentForApp; |
| version = request.version; |
| } |
| |
| @Override |
| public void writeExternal(ObjectOutput objectOutput) throws IOException { |
| objectOutput.write(version); |
| objectOutput.writeUTF(fileServiceId); |
| objectOutput.writeUTF(source.toString()); |
| objectOutput.writeUTF(destination.toString()); |
| objectOutput.write(subscriptionId); |
| objectOutput.writeUTF(appIntent); |
| } |
| |
| @Override |
| public void readExternal(ObjectInput objectInput) throws IOException { |
| version = objectInput.read(); |
| fileServiceId = objectInput.readUTF(); |
| source = Uri.parse(objectInput.readUTF()); |
| destination = Uri.parse(objectInput.readUTF()); |
| subscriptionId = objectInput.read(); |
| appIntent = objectInput.readUTF(); |
| // Do version checks here -- future versions may have other fields. |
| } |
| } |
| |
| public static class Builder { |
| private String fileServiceId; |
| private Uri source; |
| private Uri destination; |
| private int subscriptionId; |
| private String appIntent; |
| private int version = CURRENT_VERSION; |
| |
| /** |
| * Constructs a {@link Builder} from a {@link DownloadRequest} |
| * @param other The {@link DownloadRequest} from which the data for the {@link Builder} |
| * should come. |
| * @return An instance of {@link Builder} pre-populated with data from the provided |
| * {@link DownloadRequest}. |
| */ |
| public static Builder fromDownloadRequest(DownloadRequest other) { |
| Builder result = new Builder(other.sourceUri, other.destinationUri) |
| .setServiceId(other.fileServiceId) |
| .setSubscriptionId(other.subscriptionId); |
| result.appIntent = other.serializedResultIntentForApp; |
| // Version of the result is going to be the current version -- as this class gets |
| // updated, new fields will be set to default values in here. |
| return result; |
| } |
| |
| /** |
| * This method constructs a new instance of {@link Builder} based on the serialized data |
| * passed in. |
| * @param data A byte array, the contents of which should have been originally obtained |
| * from {@link DownloadRequest#toByteArray()}. |
| */ |
| public static Builder fromSerializedRequest(byte[] data) { |
| Builder builder; |
| try { |
| ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data)); |
| SerializationDataContainer dataContainer = |
| (SerializationDataContainer) stream.readObject(); |
| builder = new Builder(dataContainer.source, dataContainer.destination); |
| builder.version = dataContainer.version; |
| builder.appIntent = dataContainer.appIntent; |
| builder.fileServiceId = dataContainer.fileServiceId; |
| builder.subscriptionId = dataContainer.subscriptionId; |
| } catch (IOException e) { |
| // Really should never happen |
| Log.e(LOG_TAG, "Got IOException trying to parse opaque data"); |
| throw new IllegalArgumentException(e); |
| } catch (ClassNotFoundException e) { |
| Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data"); |
| throw new IllegalArgumentException(e); |
| } |
| return builder; |
| } |
| |
| /** |
| * Builds a new DownloadRequest. |
| * @param sourceUri the source URI for the DownloadRequest to be built. This URI should |
| * never be null. |
| * @param destinationUri The final location for the file(s) that are to be downloaded. It |
| * must be on the same filesystem as the temp file directory set via |
| * {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}. |
| * The provided path must be a directory that exists. An |
| * {@link IllegalArgumentException} will be thrown otherwise. |
| */ |
| public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) { |
| if (sourceUri == null || destinationUri == null) { |
| throw new IllegalArgumentException("Source and destination URIs must be non-null."); |
| } |
| source = sourceUri; |
| destination = destinationUri; |
| } |
| |
| /** |
| * Sets the service from which the download request to be built will download from. |
| * @param serviceInfo |
| * @return |
| */ |
| public Builder setServiceInfo(FileServiceInfo serviceInfo) { |
| fileServiceId = serviceInfo.getServiceId(); |
| return this; |
| } |
| |
| /** |
| * Set the service ID for the download request. For use by the middleware only. |
| * @hide |
| */ |
| @SystemApi |
| public Builder setServiceId(String serviceId) { |
| fileServiceId = serviceId; |
| return this; |
| } |
| |
| /** |
| * Set the subscription ID on which the file(s) should be downloaded. |
| * @param subscriptionId |
| */ |
| public Builder setSubscriptionId(int subscriptionId) { |
| this.subscriptionId = subscriptionId; |
| return this; |
| } |
| |
| /** |
| * Set the {@link Intent} that should be sent when the download completes or fails. This |
| * should be an intent with a explicit {@link android.content.ComponentName} targeted to a |
| * {@link android.content.BroadcastReceiver} in the app's package. |
| * |
| * The middleware should not use this method. |
| * @param intent |
| */ |
| public Builder setAppIntent(Intent intent) { |
| this.appIntent = intent.toUri(0); |
| if (this.appIntent.length() > MAX_APP_INTENT_SIZE) { |
| throw new IllegalArgumentException("App intent must not exceed length " + |
| MAX_APP_INTENT_SIZE); |
| } |
| return this; |
| } |
| |
| public DownloadRequest build() { |
| return new DownloadRequest(fileServiceId, source, destination, |
| subscriptionId, appIntent, version); |
| } |
| } |
| |
| private final String fileServiceId; |
| private final Uri sourceUri; |
| private final Uri destinationUri; |
| private final int subscriptionId; |
| private final String serializedResultIntentForApp; |
| private final int version; |
| |
| private DownloadRequest(String fileServiceId, |
| Uri source, Uri destination, int sub, |
| String appIntent, int version) { |
| this.fileServiceId = fileServiceId; |
| sourceUri = source; |
| subscriptionId = sub; |
| destinationUri = destination; |
| serializedResultIntentForApp = appIntent; |
| this.version = version; |
| } |
| |
| private DownloadRequest(Parcel in) { |
| fileServiceId = in.readString(); |
| sourceUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class); |
| destinationUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class); |
| subscriptionId = in.readInt(); |
| serializedResultIntentForApp = in.readString(); |
| version = in.readInt(); |
| } |
| |
| public int describeContents() { |
| return 0; |
| } |
| |
| public void writeToParcel(Parcel out, int flags) { |
| out.writeString(fileServiceId); |
| out.writeParcelable(sourceUri, flags); |
| out.writeParcelable(destinationUri, flags); |
| out.writeInt(subscriptionId); |
| out.writeString(serializedResultIntentForApp); |
| out.writeInt(version); |
| } |
| |
| /** |
| * @return The ID of the file service to download from. |
| */ |
| public String getFileServiceId() { |
| return fileServiceId; |
| } |
| |
| /** |
| * @return The source URI to download from |
| */ |
| public Uri getSourceUri() { |
| return sourceUri; |
| } |
| |
| /** |
| * @return The destination {@link Uri} of the downloaded file. |
| */ |
| public Uri getDestinationUri() { |
| return destinationUri; |
| } |
| |
| /** |
| * @return The subscription ID on which to perform MBMS operations. |
| */ |
| public int getSubscriptionId() { |
| return subscriptionId; |
| } |
| |
| /** |
| * For internal use -- returns the intent to send to the app after download completion or |
| * failure. |
| * @hide |
| */ |
| public Intent getIntentForApp() { |
| try { |
| return Intent.parseUri(serializedResultIntentForApp, 0); |
| } catch (URISyntaxException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * This method returns a byte array that may be persisted to disk and restored to a |
| * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method |
| * may be recovered via {@link Builder#fromSerializedRequest(byte[])}. |
| * @return A byte array of data to persist. |
| */ |
| public byte[] toByteArray() { |
| try { |
| ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); |
| ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); |
| SerializationDataContainer container = new SerializationDataContainer(this); |
| stream.writeObject(container); |
| stream.flush(); |
| return byteArrayOutputStream.toByteArray(); |
| } catch (IOException e) { |
| // Really should never happen |
| Log.e(LOG_TAG, "Got IOException trying to serialize opaque data"); |
| return null; |
| } |
| } |
| |
| /** @hide */ |
| public int getVersion() { |
| return version; |
| } |
| |
| public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR = |
| new Parcelable.Creator<DownloadRequest>() { |
| public DownloadRequest createFromParcel(Parcel in) { |
| return new DownloadRequest(in); |
| } |
| public DownloadRequest[] newArray(int size) { |
| return new DownloadRequest[size]; |
| } |
| }; |
| |
| /** |
| * Maximum permissible length for the app's destination path, when serialized via |
| * {@link Uri#toString()}. |
| */ |
| public static int getMaxAppIntentSize() { |
| return MAX_APP_INTENT_SIZE; |
| } |
| |
| /** |
| * Maximum permissible length for the app's download-completion intent, when serialized via |
| * {@link Intent#toUri(int)}. |
| */ |
| public static int getMaxDestinationUriSize() { |
| return MAX_DESTINATION_URI_SIZE; |
| } |
| |
| /** |
| * Retrieves the hash string that should be used as the filename when storing a token for |
| * this DownloadRequest. |
| * @hide |
| */ |
| public String getHash() { |
| MessageDigest digest; |
| try { |
| digest = MessageDigest.getInstance("SHA-256"); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException("Could not get sha256 hash object"); |
| } |
| if (version >= 1) { |
| // Hash the source, destination, and the app intent |
| digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); |
| digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8)); |
| if (serializedResultIntentForApp != null) { |
| digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); |
| } |
| } |
| // Add updates for future versions here |
| return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object o) { |
| if (this == o) return true; |
| if (o == null) { |
| return false; |
| } |
| if (!(o instanceof DownloadRequest)) { |
| return false; |
| } |
| DownloadRequest request = (DownloadRequest) o; |
| return subscriptionId == request.subscriptionId && |
| version == request.version && |
| Objects.equals(fileServiceId, request.fileServiceId) && |
| Objects.equals(sourceUri, request.sourceUri) && |
| Objects.equals(destinationUri, request.destinationUri) && |
| Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(fileServiceId, sourceUri, destinationUri, |
| subscriptionId, serializedResultIntentForApp, version); |
| } |
| } |