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.annotation.TestApi; 23 import android.content.Intent; 24 import android.net.Uri; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.util.Base64; 28 import android.util.Log; 29 30 import java.io.ByteArrayInputStream; 31 import java.io.ByteArrayOutputStream; 32 import java.io.Externalizable; 33 import java.io.File; 34 import java.io.IOException; 35 import java.io.ObjectInput; 36 import java.io.ObjectInputStream; 37 import java.io.ObjectOutput; 38 import java.io.ObjectOutputStream; 39 import java.net.URISyntaxException; 40 import java.nio.charset.StandardCharsets; 41 import java.security.MessageDigest; 42 import java.security.NoSuchAlgorithmException; 43 import java.util.Objects; 44 45 /** 46 * Describes a request to download files over cell-broadcast. Instances of this class should be 47 * created by the app when requesting a download, and instances of this class will be passed back 48 * to the app when the middleware updates the status of the download. 49 */ 50 public final class DownloadRequest implements Parcelable { 51 // Version code used to keep token calculation consistent. 52 private static final int CURRENT_VERSION = 1; 53 private static final String LOG_TAG = "MbmsDownloadRequest"; 54 55 /** @hide */ 56 public static final int MAX_APP_INTENT_SIZE = 50000; 57 58 /** @hide */ 59 public static final int MAX_DESTINATION_URI_SIZE = 50000; 60 61 /** @hide */ 62 private static class SerializationDataContainer implements Externalizable { 63 private String fileServiceId; 64 private Uri source; 65 private Uri destination; 66 private int subscriptionId; 67 private String appIntent; 68 private int version; 69 SerializationDataContainer()70 public SerializationDataContainer() {} 71 SerializationDataContainer(DownloadRequest request)72 SerializationDataContainer(DownloadRequest request) { 73 fileServiceId = request.fileServiceId; 74 source = request.sourceUri; 75 destination = request.destinationUri; 76 subscriptionId = request.subscriptionId; 77 appIntent = request.serializedResultIntentForApp; 78 version = request.version; 79 } 80 81 @Override writeExternal(ObjectOutput objectOutput)82 public void writeExternal(ObjectOutput objectOutput) throws IOException { 83 objectOutput.write(version); 84 objectOutput.writeUTF(fileServiceId); 85 objectOutput.writeUTF(source.toString()); 86 objectOutput.writeUTF(destination.toString()); 87 objectOutput.write(subscriptionId); 88 objectOutput.writeUTF(appIntent); 89 } 90 91 @Override readExternal(ObjectInput objectInput)92 public void readExternal(ObjectInput objectInput) throws IOException { 93 version = objectInput.read(); 94 fileServiceId = objectInput.readUTF(); 95 source = Uri.parse(objectInput.readUTF()); 96 destination = Uri.parse(objectInput.readUTF()); 97 subscriptionId = objectInput.read(); 98 appIntent = objectInput.readUTF(); 99 // Do version checks here -- future versions may have other fields. 100 } 101 } 102 103 public static class Builder { 104 private String fileServiceId; 105 private Uri source; 106 private Uri destination; 107 private int subscriptionId; 108 private String appIntent; 109 private int version = CURRENT_VERSION; 110 111 /** 112 * Constructs a {@link Builder} from a {@link DownloadRequest} 113 * @param other The {@link DownloadRequest} from which the data for the {@link Builder} 114 * should come. 115 * @return An instance of {@link Builder} pre-populated with data from the provided 116 * {@link DownloadRequest}. 117 */ fromDownloadRequest(DownloadRequest other)118 public static Builder fromDownloadRequest(DownloadRequest other) { 119 Builder result = new Builder(other.sourceUri, other.destinationUri) 120 .setServiceId(other.fileServiceId) 121 .setSubscriptionId(other.subscriptionId); 122 result.appIntent = other.serializedResultIntentForApp; 123 // Version of the result is going to be the current version -- as this class gets 124 // updated, new fields will be set to default values in here. 125 return result; 126 } 127 128 /** 129 * This method constructs a new instance of {@link Builder} based on the serialized data 130 * passed in. 131 * @param data A byte array, the contents of which should have been originally obtained 132 * from {@link DownloadRequest#toByteArray()}. 133 */ fromSerializedRequest(byte[] data)134 public static Builder fromSerializedRequest(byte[] data) { 135 Builder builder; 136 try { 137 ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(data)); 138 SerializationDataContainer dataContainer = 139 (SerializationDataContainer) stream.readObject(); 140 builder = new Builder(dataContainer.source, dataContainer.destination); 141 builder.version = dataContainer.version; 142 builder.appIntent = dataContainer.appIntent; 143 builder.fileServiceId = dataContainer.fileServiceId; 144 builder.subscriptionId = dataContainer.subscriptionId; 145 } catch (IOException e) { 146 // Really should never happen 147 Log.e(LOG_TAG, "Got IOException trying to parse opaque data"); 148 throw new IllegalArgumentException(e); 149 } catch (ClassNotFoundException e) { 150 Log.e(LOG_TAG, "Got ClassNotFoundException trying to parse opaque data"); 151 throw new IllegalArgumentException(e); 152 } 153 return builder; 154 } 155 156 /** 157 * Builds a new DownloadRequest. 158 * @param sourceUri the source URI for the DownloadRequest to be built. This URI should 159 * never be null. 160 * @param destinationUri The final location for the file(s) that are to be downloaded. It 161 * must be on the same filesystem as the temp file directory set via 162 * {@link android.telephony.MbmsDownloadSession#setTempFileRootDirectory(File)}. 163 * The provided path must be a directory that exists. An 164 * {@link IllegalArgumentException} will be thrown otherwise. 165 */ Builder(@onNull Uri sourceUri, @NonNull Uri destinationUri)166 public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri) { 167 if (sourceUri == null || destinationUri == null) { 168 throw new IllegalArgumentException("Source and destination URIs must be non-null."); 169 } 170 source = sourceUri; 171 destination = destinationUri; 172 } 173 174 /** 175 * Sets the service from which the download request to be built will download from. 176 * @param serviceInfo 177 * @return 178 */ setServiceInfo(FileServiceInfo serviceInfo)179 public Builder setServiceInfo(FileServiceInfo serviceInfo) { 180 fileServiceId = serviceInfo.getServiceId(); 181 return this; 182 } 183 184 /** 185 * Set the service ID for the download request. For use by the middleware only. 186 * @hide 187 */ 188 @SystemApi 189 @TestApi setServiceId(String serviceId)190 public Builder setServiceId(String serviceId) { 191 fileServiceId = serviceId; 192 return this; 193 } 194 195 /** 196 * Set the subscription ID on which the file(s) should be downloaded. 197 * @param subscriptionId 198 */ setSubscriptionId(int subscriptionId)199 public Builder setSubscriptionId(int subscriptionId) { 200 this.subscriptionId = subscriptionId; 201 return this; 202 } 203 204 /** 205 * Set the {@link Intent} that should be sent when the download completes or fails. This 206 * should be an intent with a explicit {@link android.content.ComponentName} targeted to a 207 * {@link android.content.BroadcastReceiver} in the app's package. 208 * 209 * The middleware should not use this method. 210 * @param intent 211 */ setAppIntent(Intent intent)212 public Builder setAppIntent(Intent intent) { 213 this.appIntent = intent.toUri(0); 214 if (this.appIntent.length() > MAX_APP_INTENT_SIZE) { 215 throw new IllegalArgumentException("App intent must not exceed length " + 216 MAX_APP_INTENT_SIZE); 217 } 218 return this; 219 } 220 build()221 public DownloadRequest build() { 222 return new DownloadRequest(fileServiceId, source, destination, 223 subscriptionId, appIntent, version); 224 } 225 } 226 227 private final String fileServiceId; 228 private final Uri sourceUri; 229 private final Uri destinationUri; 230 private final int subscriptionId; 231 private final String serializedResultIntentForApp; 232 private final int version; 233 DownloadRequest(String fileServiceId, Uri source, Uri destination, int sub, String appIntent, int version)234 private DownloadRequest(String fileServiceId, 235 Uri source, Uri destination, int sub, 236 String appIntent, int version) { 237 this.fileServiceId = fileServiceId; 238 sourceUri = source; 239 subscriptionId = sub; 240 destinationUri = destination; 241 serializedResultIntentForApp = appIntent; 242 this.version = version; 243 } 244 DownloadRequest(Parcel in)245 private DownloadRequest(Parcel in) { 246 fileServiceId = in.readString(); 247 sourceUri = in.readParcelable(getClass().getClassLoader()); 248 destinationUri = in.readParcelable(getClass().getClassLoader()); 249 subscriptionId = in.readInt(); 250 serializedResultIntentForApp = in.readString(); 251 version = in.readInt(); 252 } 253 describeContents()254 public int describeContents() { 255 return 0; 256 } 257 writeToParcel(Parcel out, int flags)258 public void writeToParcel(Parcel out, int flags) { 259 out.writeString(fileServiceId); 260 out.writeParcelable(sourceUri, flags); 261 out.writeParcelable(destinationUri, flags); 262 out.writeInt(subscriptionId); 263 out.writeString(serializedResultIntentForApp); 264 out.writeInt(version); 265 } 266 267 /** 268 * @return The ID of the file service to download from. 269 */ getFileServiceId()270 public String getFileServiceId() { 271 return fileServiceId; 272 } 273 274 /** 275 * @return The source URI to download from 276 */ getSourceUri()277 public Uri getSourceUri() { 278 return sourceUri; 279 } 280 281 /** 282 * @return The destination {@link Uri} of the downloaded file. 283 */ getDestinationUri()284 public Uri getDestinationUri() { 285 return destinationUri; 286 } 287 288 /** 289 * @return The subscription ID on which to perform MBMS operations. 290 */ getSubscriptionId()291 public int getSubscriptionId() { 292 return subscriptionId; 293 } 294 295 /** 296 * For internal use -- returns the intent to send to the app after download completion or 297 * failure. 298 * @hide 299 */ getIntentForApp()300 public Intent getIntentForApp() { 301 try { 302 return Intent.parseUri(serializedResultIntentForApp, 0); 303 } catch (URISyntaxException e) { 304 return null; 305 } 306 } 307 308 /** 309 * This method returns a byte array that may be persisted to disk and restored to a 310 * {@link DownloadRequest}. The instance of {@link DownloadRequest} persisted by this method 311 * may be recovered via {@link Builder#fromSerializedRequest(byte[])}. 312 * @return A byte array of data to persist. 313 */ toByteArray()314 public byte[] toByteArray() { 315 try { 316 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 317 ObjectOutputStream stream = new ObjectOutputStream(byteArrayOutputStream); 318 SerializationDataContainer container = new SerializationDataContainer(this); 319 stream.writeObject(container); 320 stream.flush(); 321 return byteArrayOutputStream.toByteArray(); 322 } catch (IOException e) { 323 // Really should never happen 324 Log.e(LOG_TAG, "Got IOException trying to serialize opaque data"); 325 return null; 326 } 327 } 328 329 /** @hide */ getVersion()330 public int getVersion() { 331 return version; 332 } 333 334 public static final @android.annotation.NonNull Parcelable.Creator<DownloadRequest> CREATOR = 335 new Parcelable.Creator<DownloadRequest>() { 336 public DownloadRequest createFromParcel(Parcel in) { 337 return new DownloadRequest(in); 338 } 339 public DownloadRequest[] newArray(int size) { 340 return new DownloadRequest[size]; 341 } 342 }; 343 344 /** 345 * Maximum permissible length for the app's destination path, when serialized via 346 * {@link Uri#toString()}. 347 */ getMaxAppIntentSize()348 public static int getMaxAppIntentSize() { 349 return MAX_APP_INTENT_SIZE; 350 } 351 352 /** 353 * Maximum permissible length for the app's download-completion intent, when serialized via 354 * {@link Intent#toUri(int)}. 355 */ getMaxDestinationUriSize()356 public static int getMaxDestinationUriSize() { 357 return MAX_DESTINATION_URI_SIZE; 358 } 359 360 /** 361 * Retrieves the hash string that should be used as the filename when storing a token for 362 * this DownloadRequest. 363 * @hide 364 */ getHash()365 public String getHash() { 366 MessageDigest digest; 367 try { 368 digest = MessageDigest.getInstance("SHA-256"); 369 } catch (NoSuchAlgorithmException e) { 370 throw new RuntimeException("Could not get sha256 hash object"); 371 } 372 if (version >= 1) { 373 // Hash the source, destination, and the app intent 374 digest.update(sourceUri.toString().getBytes(StandardCharsets.UTF_8)); 375 digest.update(destinationUri.toString().getBytes(StandardCharsets.UTF_8)); 376 if (serializedResultIntentForApp != null) { 377 digest.update(serializedResultIntentForApp.getBytes(StandardCharsets.UTF_8)); 378 } 379 } 380 // Add updates for future versions here 381 return Base64.encodeToString(digest.digest(), Base64.URL_SAFE | Base64.NO_WRAP); 382 } 383 384 @Override equals(@ullable Object o)385 public boolean equals(@Nullable Object o) { 386 if (this == o) return true; 387 if (o == null) { 388 return false; 389 } 390 if (!(o instanceof DownloadRequest)) { 391 return false; 392 } 393 DownloadRequest request = (DownloadRequest) o; 394 return subscriptionId == request.subscriptionId && 395 version == request.version && 396 Objects.equals(fileServiceId, request.fileServiceId) && 397 Objects.equals(sourceUri, request.sourceUri) && 398 Objects.equals(destinationUri, request.destinationUri) && 399 Objects.equals(serializedResultIntentForApp, request.serializedResultIntentForApp); 400 } 401 402 @Override hashCode()403 public int hashCode() { 404 return Objects.hash(fileServiceId, sourceUri, destinationUri, 405 subscriptionId, serializedResultIntentForApp, version); 406 } 407 } 408