blob: eae9011e42c8ac6f766f9dca2d7a3e52b08ae0df [file] [log] [blame]
/*
* 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.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.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
/**
* A Parcelable class describing a pending Cell-Broadcast download request
* @hide
*/
public 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";
/**
* Maximum permissible length for the app's download-completion intent, when serialized via
* {@link Intent#toUri(int)}.
*/
public static final int MAX_APP_INTENT_SIZE = 50000;
/**
* Maximum permissible length for the app's destination path, when serialized via
* {@link Uri#toString()}.
*/
public static final int MAX_DESTINATION_URI_SIZE = 50000;
/** @hide */
private static class OpaqueDataContainer implements Serializable {
private final String destinationUri;
private final String appIntent;
private final int version;
public OpaqueDataContainer(String destinationUri, String appIntent, int version) {
this.destinationUri = destinationUri;
this.appIntent = appIntent;
this.version = version;
}
}
public static class Builder {
private String fileServiceId;
private Uri source;
private Uri dest;
private int subscriptionId;
private String appIntent;
private int version = CURRENT_VERSION;
/**
* 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
* TODO: systemapi
*/
public Builder setServiceId(String serviceId) {
fileServiceId = serviceId;
return this;
}
/**
* Sets the source URI for the download request to be built.
* @param source
* @return
*/
public Builder setSource(Uri source) {
this.source = source;
return this;
}
/**
* Sets the destination URI for the download request to be built. The middleware should
* not set this directly.
* @param dest A URI obtained from {@link Uri#fromFile(File)}, denoting the requested
* final destination of the download.
* @return
*/
public Builder setDest(Uri dest) {
if (dest.toString().length() > MAX_DESTINATION_URI_SIZE) {
throw new IllegalArgumentException("Destination uri must not exceed length " +
MAX_DESTINATION_URI_SIZE);
}
this.dest = dest;
return this;
}
/**
* Set the subscription ID on which the file(s) should be downloaded.
* @param subscriptionId
* @return
*/
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
* @return
*/
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;
}
/**
* For use by the middleware to set the byte array of opaque data. The opaque data
* includes information about the download request that is used by the client app and the
* manager code, but is irrelevant to the middleware.
* @param data A byte array, the contents of which should have been originally obtained
* from {@link DownloadRequest#getOpaqueData()}.
* @return
* TODO: systemapi
* @hide
*/
public Builder setOpaqueData(byte[] data) {
try {
ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data));
OpaqueDataContainer dataContainer = (OpaqueDataContainer) stream.readObject();
version = dataContainer.version;
appIntent = dataContainer.appIntent;
dest = Uri.parse(dataContainer.destinationUri);
} 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 this;
}
public DownloadRequest build() {
return new DownloadRequest(fileServiceId, source, dest,
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 dest,
int sub, String appIntent, int version) {
this.fileServiceId = fileServiceId;
sourceUri = source;
destinationUri = dest;
subscriptionId = sub;
serializedResultIntentForApp = appIntent;
this.version = version;
}
public static DownloadRequest copy(DownloadRequest other) {
return new DownloadRequest(other);
}
private DownloadRequest(DownloadRequest dr) {
fileServiceId = dr.fileServiceId;
sourceUri = dr.sourceUri;
destinationUri = dr.destinationUri;
subscriptionId = dr.subscriptionId;
serializedResultIntentForApp = dr.serializedResultIntentForApp;
version = dr.version;
}
private DownloadRequest(Parcel in) {
fileServiceId = in.readString();
sourceUri = in.readParcelable(getClass().getClassLoader());
destinationUri = in.readParcelable(getClass().getClassLoader());
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;
}
/**
* For use by the client app only.
* @return The URI of the final destination of the download.
*/
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;
}
}
/**
* For use by the middleware only. The byte array returned from this method should be
* persisted and sent back to the app upon download completion or failure by passing it into
* {@link Builder#setOpaqueData(byte[])}.
* @return A byte array of opaque data to persist.
* @hide
* TODO: systemapi
*/
public byte[] getOpaqueData() {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream);
OpaqueDataContainer container = new OpaqueDataContainer(
destinationUri.toString(), serializedResultIntentForApp, version);
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 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];
}
};
/**
* @hide
*/
public boolean isMultipartDownload() {
// TODO: figure out what qualifies a request as a multipart download request.
return getSourceUri().getLastPathSegment() != null &&
getSourceUri().getLastPathSegment().contains("*");
}
/**
* 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 URI, destination URI, and the app intent
digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8));
digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8));
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(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);
}
}