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.content.Intent; 23 import android.net.Uri; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.util.Base64; 27 import android.util.Log; 28 29 import java.io.ByteArrayInputStream; 30 import java.io.ByteArrayOutputStream; 31 import java.io.Externalizable; 32 import java.io.File; 33 import java.io.IOException; 34 import java.io.ObjectInput; 35 import java.io.ObjectInputStream; 36 import java.io.ObjectOutput; 37 import java.io.ObjectOutputStream; 38 import java.net.URISyntaxException; 39 import java.nio.charset.StandardCharsets; 40 import java.security.MessageDigest; 41 import java.security.NoSuchAlgorithmException; 42 import 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 */ 49 public 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 SerializationDataContainer()69 public SerializationDataContainer() {} 70 SerializationDataContainer(DownloadRequest request)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 writeExternal(ObjectOutput objectOutput)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 readExternal(ObjectInput objectInput)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 */ fromDownloadRequest(DownloadRequest other)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 */ fromSerializedRequest(byte[] data)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 */ Builder(@onNull Uri sourceUri, @NonNull Uri destinationUri)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 */ setServiceInfo(FileServiceInfo serviceInfo)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 setServiceId(String serviceId)188 public Builder setServiceId(String serviceId) { 189 fileServiceId = serviceId; 190 return this; 191 } 192 193 /** 194 * Set the subscription ID on which the file(s) should be downloaded. 195 * @param subscriptionId 196 */ setSubscriptionId(int subscriptionId)197 public Builder setSubscriptionId(int subscriptionId) { 198 this.subscriptionId = subscriptionId; 199 return this; 200 } 201 202 /** 203 * Set the {@link Intent} that should be sent when the download completes or fails. This 204 * should be an intent with a explicit {@link android.content.ComponentName} targeted to a 205 * {@link android.content.BroadcastReceiver} in the app's package. 206 * 207 * The middleware should not use this method. 208 * @param intent 209 */ setAppIntent(Intent intent)210 public Builder setAppIntent(Intent intent) { 211 this.appIntent = intent.toUri(0); 212 if (this.appIntent.length() > MAX_APP_INTENT_SIZE) { 213 throw new IllegalArgumentException("App intent must not exceed length " + 214 MAX_APP_INTENT_SIZE); 215 } 216 return this; 217 } 218 build()219 public DownloadRequest build() { 220 return new DownloadRequest(fileServiceId, source, destination, 221 subscriptionId, appIntent, version); 222 } 223 } 224 225 private final String fileServiceId; 226 private final Uri sourceUri; 227 private final Uri destinationUri; 228 private final int subscriptionId; 229 private final String serializedResultIntentForApp; 230 private final int version; 231 DownloadRequest(String fileServiceId, Uri source, Uri destination, int sub, String appIntent, int version)232 private DownloadRequest(String fileServiceId, 233 Uri source, Uri destination, int sub, 234 String appIntent, int version) { 235 this.fileServiceId = fileServiceId; 236 sourceUri = source; 237 subscriptionId = sub; 238 destinationUri = destination; 239 serializedResultIntentForApp = appIntent; 240 this.version = version; 241 } 242 DownloadRequest(Parcel in)243 private DownloadRequest(Parcel in) { 244 fileServiceId = in.readString(); 245 sourceUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class); 246 destinationUri = in.readParcelable(getClass().getClassLoader(), android.net.Uri.class); 247 subscriptionId = in.readInt(); 248 serializedResultIntentForApp = in.readString(); 249 version = in.readInt(); 250 } 251 describeContents()252 public int describeContents() { 253 return 0; 254 } 255 writeToParcel(Parcel out, int flags)256 public void writeToParcel(Parcel out, int flags) { 257 out.writeString(fileServiceId); 258 out.writeParcelable(sourceUri, flags); 259 out.writeParcelable(destinationUri, flags); 260 out.writeInt(subscriptionId); 261 out.writeString(serializedResultIntentForApp); 262 out.writeInt(version); 263 } 264 265 /** 266 * @return The ID of the file service to download from. 267 */ getFileServiceId()268 public String getFileServiceId() { 269 return fileServiceId; 270 } 271 272 /** 273 * @return The source URI to download from 274 */ getSourceUri()275 public Uri getSourceUri() { 276 return sourceUri; 277 } 278 279 /** 280 * @return The destination {@link Uri} of the downloaded file. 281 */ getDestinationUri()282 public Uri getDestinationUri() { 283 return destinationUri; 284 } 285 286 /** 287 * @return The subscription ID on which to perform MBMS operations. 288 */ getSubscriptionId()289 public int getSubscriptionId() { 290 return subscriptionId; 291 } 292 293 /** 294 * For internal use -- returns the intent to send to the app after download completion or 295 * failure. 296 * @hide 297 */ getIntentForApp()298 public Intent getIntentForApp() { 299 try { 300 return Intent.parseUri(serializedResultIntentForApp, 0); 301 } catch (URISyntaxException e) { 302 return null; 303 } 304 } 305 306 /** 307 * This method returns a byte array that may be persisted to disk and restored to a 308 * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method 309 * may be recovered via {@link Builder#fromSerializedRequest(byte[])}. 310 * @return A byte array of data to persist. 311 */ toByteArray()312 public byte[] toByteArray() { 313 try { 314 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 315 ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); 316 SerializationDataContainer container = new SerializationDataContainer(this); 317 stream.writeObject(container); 318 stream.flush(); 319 return byteArrayOutputStream.toByteArray(); 320 } catch (IOException e) { 321 // Really should never happen 322 Log.e(LOG_TAG, "Got IOException trying to serialize opaque data"); 323 return null; 324 } 325 } 326 327 /** @hide */ getVersion()328 public int getVersion() { 329 return version; 330 } 331 332 public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR = 333 new Parcelable.Creator<DownloadRequest>() { 334 public DownloadRequest createFromParcel(Parcel in) { 335 return new DownloadRequest(in); 336 } 337 public DownloadRequest[] newArray(int size) { 338 return new DownloadRequest[size]; 339 } 340 }; 341 342 /** 343 * Maximum permissible length for the app's destination path, when serialized via 344 * {@link Uri#toString()}. 345 */ getMaxAppIntentSize()346 public static int getMaxAppIntentSize() { 347 return MAX_APP_INTENT_SIZE; 348 } 349 350 /** 351 * Maximum permissible length for the app's download-completion intent, when serialized via 352 * {@link Intent#toUri(int)}. 353 */ getMaxDestinationUriSize()354 public static int getMaxDestinationUriSize() { 355 return MAX_DESTINATION_URI_SIZE; 356 } 357 358 /** 359 * Retrieves the hash string that should be used as the filename when storing a token for 360 * this DownloadRequest. 361 * @hide 362 */ getHash()363 public String getHash() { 364 MessageDigest digest; 365 try { 366 digest = MessageDigest.getInstance("SHA-256"); 367 } catch (NoSuchAlgorithmException e) { 368 throw new RuntimeException("Could not get sha256 hash object"); 369 } 370 if (version >= 1) { 371 // Hash the source, destination, and the app intent 372 digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); 373 digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8)); 374 if (serializedResultIntentForApp != null) { 375 digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); 376 } 377 } 378 // Add updates for future versions here 379 return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP); 380 } 381 382 @Override equals(@ullable Object o)383 public boolean equals(@Nullable Object o) { 384 if (this == o) return true; 385 if (o == null) { 386 return false; 387 } 388 if (!(o instanceof DownloadRequest)) { 389 return false; 390 } 391 DownloadRequest request = (DownloadRequest) o; 392 return subscriptionId == request.subscriptionId && 393 version == request.version && 394 Objects.equals(fileServiceId, request.fileServiceId) && 395 Objects.equals(sourceUri, request.sourceUri) && 396 Objects.equals(destinationUri, request.destinationUri) && 397 Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp); 398 } 399 400 @Override hashCode()401 public int hashCode() { 402 return Objects.hash(fileServiceId, sourceUri, destinationUri, 403 subscriptionId, serializedResultIntentForApp, version); 404 } 405 } 406