406 lines
15 KiB
Java
406 lines
15 KiB
Java
![]() |
/*
|
||
|
* 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);
|
||
|
}
|
||
|
}
|