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