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