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.cts.embmstestapp; 18 19 import android.app.Activity; 20 import android.app.Service; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.net.Uri; 26 import android.os.Binder; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.IBinder; 31 import android.os.ParcelFileDescriptor; 32 import android.os.RemoteException; 33 import android.telephony.MbmsDownloadSession; 34 import android.telephony.mbms.DownloadProgressListener; 35 import android.telephony.mbms.DownloadRequest; 36 import android.telephony.mbms.DownloadStatusListener; 37 import android.telephony.mbms.FileInfo; 38 import android.telephony.mbms.FileServiceInfo; 39 import android.telephony.mbms.MbmsDownloadSessionCallback; 40 import android.telephony.mbms.MbmsErrors; 41 import android.telephony.mbms.UriPathPair; 42 import android.telephony.mbms.vendor.MbmsDownloadServiceBase; 43 import android.telephony.mbms.vendor.VendorUtils; 44 import android.util.Log; 45 46 import java.io.IOException; 47 import java.io.OutputStream; 48 import java.util.ArrayList; 49 import java.util.Arrays; 50 import java.util.Collections; 51 import java.util.Date; 52 import java.util.HashMap; 53 import java.util.LinkedList; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 import java.util.Set; 58 59 public class CtsDownloadService extends Service { 60 private static final Set<String> ALLOWED_PACKAGES = Set.of("android.telephony.cts"); 61 private static final String TAG = "EmbmsTestDownload"; 62 63 public static final String METHOD_NAME = "method_name"; 64 public static final String METHOD_INITIALIZE = "initialize"; 65 public static final String METHOD_REQUEST_UPDATE_FILE_SERVICES = 66 "requestUpdateFileServices"; 67 public static final String METHOD_ADD_SERVICE_ANNOUNCEMENT = "addServiceAnnouncementFile"; 68 public static final String METHOD_SET_TEMP_FILE_ROOT = "setTempFileRootDirectory"; 69 public static final String METHOD_RESET_DOWNLOAD_KNOWLEDGE = "resetDownloadKnowledge"; 70 public static final String METHOD_GET_DOWNLOAD_STATUS = "getDownloadStatus"; 71 public static final String METHOD_CANCEL_DOWNLOAD = "cancelDownload"; 72 public static final String METHOD_CLOSE = "close"; 73 // Not a method call, but it's a form of communication to the middleware so it's included 74 // here for convenience. 75 public static final String METHOD_DOWNLOAD_RESULT_ACK = "downloadResultAck"; 76 77 public static final String ARGUMENT_SUBSCRIPTION_ID = "subscriptionId"; 78 public static final String ARGUMENT_SERVICE_CLASSES = "serviceClasses"; 79 public static final String ARGUMENT_ROOT_DIRECTORY_PATH = "rootDirectoryPath"; 80 public static final String ARGUMENT_DOWNLOAD_REQUEST = "downloadRequest"; 81 public static final String ARGUMENT_FILE_INFO = "fileInfo"; 82 public static final String ARGUMENT_RESULT_CODE = "resultCode"; 83 public static final String ARGUMENT_SERVICE_ANNOUNCEMENT_FILE = "serviceAnnouncementFile"; 84 85 public static final String CONTROL_INTERFACE_ACTION = 86 "android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE"; 87 public static final ComponentName CONTROL_INTERFACE_COMPONENT = 88 ComponentName.unflattenFromString( 89 "android.telephony.cts.embmstestapp/.CtsDownloadService"); 90 public static final ComponentName CTS_TEST_RECEIVER_COMPONENT = 91 ComponentName.unflattenFromString( 92 "android.telephony.cts/android.telephony.mbms.MbmsDownloadReceiver"); 93 94 public static final Uri DOWNLOAD_SOURCE_URI_ROOT = 95 Uri.parse("http://www.example.com/file_download"); 96 public static final FileServiceInfo FILE_SERVICE_INFO; 97 public static final FileInfo FILE_INFO_1 = new FileInfo( 98 DOWNLOAD_SOURCE_URI_ROOT.buildUpon().appendPath("file1.txt").build(), 99 "text/plain"); 100 public static final FileInfo FILE_INFO_2 = new FileInfo( 101 DOWNLOAD_SOURCE_URI_ROOT.buildUpon().appendPath("sub_dir1") 102 .appendPath("sub_dir2") 103 .appendPath("file2.txt") 104 .build(), 105 "text/plain"); 106 public static final byte[] SAMPLE_FILE_DATA = "this is some sample file data".getBytes(); 107 108 // Define allowed source URIs so that we don't have to do the prefix matching calculation 109 public static final Uri SOURCE_URI_1 = DOWNLOAD_SOURCE_URI_ROOT.buildUpon() 110 .appendPath("file1.txt").build(); 111 public static final Uri SOURCE_URI_2 = DOWNLOAD_SOURCE_URI_ROOT.buildUpon() 112 .appendPath("sub_dir1").appendPath("*").build(); 113 public static final Uri SOURCE_URI_3 = DOWNLOAD_SOURCE_URI_ROOT.buildUpon() 114 .appendPath("*").build(); 115 116 static { 117 String id = "urn:3GPP:service_0-0"; 118 Map<Locale, String> localeDict = Map.of( 119 Locale.US, "Entertainment Source 1", 120 Locale.CANADA, "Entertainment Source 1, eh?"); 121 List<Locale> locales = Arrays.asList(Locale.CANADA, Locale.US); 122 List<FileInfo> files = Arrays.asList(FILE_INFO_1, FILE_INFO_2); 123 FILE_SERVICE_INFO = new FileServiceInfo(localeDict, "class1", locales, 124 id, new Date(2017, 8, 21, 18, 20, 29), 125 new Date(2017, 8, 21, 18, 23, 9), files); 126 } 127 128 private MbmsDownloadSessionCallback mAppCallback; 129 private DownloadStatusListener mDownloadStatusListener; 130 private DownloadProgressListener mDownloadProgressListener; 131 132 private HandlerThread mHandlerThread; 133 private Handler mHandler; 134 private List<Bundle> mReceivedCalls = new LinkedList<>(); 135 private int mErrorCodeOverride = MbmsErrors.SUCCESS; 136 private List<DownloadRequest> mReceivedRequests = new LinkedList<>(); 137 private String mTempFileRootDirPath = null; 138 139 private final MbmsDownloadServiceBase mDownloadServiceImpl = new MbmsDownloadServiceBase() { 140 @Override 141 public int initialize(int subscriptionId, MbmsDownloadSessionCallback callback) 142 throws RemoteException { 143 super.initialize(subscriptionId, callback); // noop to placate the coverage tool 144 Bundle b = new Bundle(); 145 b.putString(METHOD_NAME, METHOD_INITIALIZE); 146 b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); 147 mReceivedCalls.add(b); 148 149 if (mErrorCodeOverride != MbmsErrors.SUCCESS) { 150 return mErrorCodeOverride; 151 } 152 153 int packageUid = Binder.getCallingUid(); 154 String[] packageNames = getPackageManager().getPackagesForUid(packageUid); 155 if (packageNames == null) { 156 return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; 157 } 158 boolean isUidAllowed = Arrays.stream(packageNames).anyMatch(ALLOWED_PACKAGES::contains); 159 if (!isUidAllowed) { 160 return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; 161 } 162 163 mHandler.post(() -> { 164 if (mAppCallback == null) { 165 mAppCallback = callback; 166 } else { 167 callback.onError( 168 MbmsErrors.InitializationErrors.ERROR_DUPLICATE_INITIALIZE, ""); 169 return; 170 } 171 callback.onMiddlewareReady(); 172 }); 173 return MbmsErrors.SUCCESS; 174 } 175 176 @Override 177 public int requestUpdateFileServices(int subscriptionId, List<String> serviceClasses) 178 throws RemoteException { 179 // noop to placate the coverage tool 180 super.requestUpdateFileServices(subscriptionId, serviceClasses); 181 182 Bundle b = new Bundle(); 183 b.putString(METHOD_NAME, METHOD_REQUEST_UPDATE_FILE_SERVICES); 184 b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); 185 b.putStringArrayList(ARGUMENT_SERVICE_CLASSES, new ArrayList<>(serviceClasses)); 186 mReceivedCalls.add(b); 187 188 if (mErrorCodeOverride != MbmsErrors.SUCCESS) { 189 return mErrorCodeOverride; 190 } 191 192 List<FileServiceInfo> serviceInfos = Collections.singletonList(FILE_SERVICE_INFO); 193 194 mHandler.post(() -> { 195 if (mAppCallback!= null) { 196 mAppCallback.onFileServicesUpdated(serviceInfos); 197 } 198 }); 199 200 return MbmsErrors.SUCCESS; 201 } 202 203 @Override 204 public int download(DownloadRequest downloadRequest) throws RemoteException { 205 super.download(downloadRequest); // noop to placate the coverage tool 206 mReceivedRequests.add(downloadRequest); 207 return MbmsErrors.SUCCESS; 208 } 209 210 @Override 211 public int setTempFileRootDirectory(int subscriptionId, String rootDirectoryPath) 212 throws RemoteException { 213 // noop to placate the coverage tool 214 super.setTempFileRootDirectory(subscriptionId, rootDirectoryPath); 215 if (mErrorCodeOverride != MbmsErrors.SUCCESS) { 216 return mErrorCodeOverride; 217 } 218 219 Bundle b = new Bundle(); 220 b.putString(METHOD_NAME, METHOD_SET_TEMP_FILE_ROOT); 221 b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); 222 b.putString(ARGUMENT_ROOT_DIRECTORY_PATH, rootDirectoryPath); 223 mReceivedCalls.add(b); 224 mTempFileRootDirPath = rootDirectoryPath; 225 return 0; 226 } 227 228 @Override 229 public int addProgressListener(DownloadRequest downloadRequest, 230 DownloadProgressListener listener) throws RemoteException { 231 // noop to placate the coverage tool 232 super.addProgressListener(downloadRequest, listener); 233 mDownloadProgressListener = listener; 234 return MbmsErrors.SUCCESS; 235 } 236 237 @Override 238 public int addStatusListener(DownloadRequest downloadRequest, 239 DownloadStatusListener listener) throws RemoteException { 240 // noop to placate the coverage tool 241 super.addStatusListener(downloadRequest, listener); 242 mDownloadStatusListener = listener; 243 return MbmsErrors.SUCCESS; 244 } 245 246 @Override 247 public void dispose(int subscriptionId) throws RemoteException { 248 // noop to placate the coverage tool 249 super.dispose(subscriptionId); 250 Bundle b = new Bundle(); 251 b.putString(METHOD_NAME, METHOD_CLOSE); 252 b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); 253 mReceivedCalls.add(b); 254 } 255 256 @Override 257 public int requestDownloadState(DownloadRequest downloadRequest, FileInfo fileInfo) 258 throws RemoteException { 259 // noop to placate the coverage tool 260 super.requestDownloadState(downloadRequest, fileInfo); 261 Bundle b = new Bundle(); 262 b.putString(METHOD_NAME, METHOD_GET_DOWNLOAD_STATUS); 263 b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest); 264 b.putParcelable(ARGUMENT_FILE_INFO, fileInfo); 265 mReceivedCalls.add(b); 266 return MbmsDownloadSession.STATUS_ACTIVELY_DOWNLOADING; 267 } 268 269 @Override 270 public int addServiceAnnouncement(int subscriptionId, byte[] announcementFile) { 271 try { 272 // noop to placate the coverage tool 273 super.addServiceAnnouncement(subscriptionId, announcementFile); 274 } catch (UnsupportedOperationException e) { 275 // expected 276 } 277 Bundle b = new Bundle(); 278 b.putString(METHOD_NAME, METHOD_ADD_SERVICE_ANNOUNCEMENT); 279 b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); 280 b.putByteArray(ARGUMENT_SERVICE_ANNOUNCEMENT_FILE, announcementFile); 281 mReceivedCalls.add(b); 282 return MbmsErrors.SUCCESS; 283 } 284 285 @Override 286 public int cancelDownload(DownloadRequest request) throws RemoteException { 287 // noop to placate the coverage tool 288 super.cancelDownload(request); 289 Bundle b = new Bundle(); 290 b.putString(METHOD_NAME, METHOD_CANCEL_DOWNLOAD); 291 b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, request); 292 mReceivedCalls.add(b); 293 mReceivedRequests.remove(request); 294 return MbmsErrors.SUCCESS; 295 } 296 297 @Override 298 public List<DownloadRequest> listPendingDownloads(int subscriptionId) 299 throws RemoteException { 300 // noop to placate the coverage tool 301 super.listPendingDownloads(subscriptionId); 302 return mReceivedRequests; 303 } 304 305 @Override 306 public int removeStatusListener(DownloadRequest downloadRequest, 307 DownloadStatusListener callback) throws RemoteException { 308 // noop to placate the coverage tool 309 super.removeStatusListener(downloadRequest, callback); 310 mDownloadStatusListener = null; 311 return MbmsErrors.SUCCESS; 312 } 313 314 @Override 315 public int resetDownloadKnowledge(DownloadRequest downloadRequest) throws RemoteException { 316 // noop to placate the coverage tool 317 super.resetDownloadKnowledge(downloadRequest); 318 Bundle b = new Bundle(); 319 b.putString(METHOD_NAME, METHOD_RESET_DOWNLOAD_KNOWLEDGE); 320 b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest); 321 mReceivedCalls.add(b); 322 return MbmsErrors.SUCCESS; 323 } 324 325 @Override 326 public void onAppCallbackDied(int uid, int subscriptionId) { 327 // noop to placate the coverage tool 328 super.onAppCallbackDied(uid, subscriptionId); 329 mAppCallback = null; 330 } 331 }; 332 333 private final IBinder mControlInterface = new ICtsDownloadMiddlewareControl.Stub() { 334 @Override 335 public void reset() { 336 mReceivedCalls.clear(); 337 mHandler.removeCallbacksAndMessages(null); 338 mAppCallback = null; 339 mErrorCodeOverride = MbmsErrors.SUCCESS; 340 mReceivedRequests.clear(); 341 mDownloadStatusListener = null; 342 mTempFileRootDirPath = null; 343 } 344 345 @Override 346 public List<Bundle> getDownloadSessionCalls() { 347 return mReceivedCalls; 348 } 349 350 @Override 351 public void forceErrorCode(int error) { 352 mErrorCodeOverride = error; 353 } 354 355 @Override 356 public void fireErrorOnSession(int errorCode, String message) { 357 mHandler.post(() -> mAppCallback.onError(errorCode, message)); 358 } 359 360 @Override 361 public void fireOnProgressUpdated(DownloadRequest request, FileInfo fileInfo, 362 int currentDownloadSize, int fullDownloadSize, 363 int currentDecodedSize, int fullDecodedSize) { 364 if (mDownloadStatusListener == null) { 365 return; 366 } 367 mHandler.post(() -> mDownloadProgressListener.onProgressUpdated(request, fileInfo, 368 currentDownloadSize, fullDownloadSize, currentDecodedSize, fullDecodedSize)); 369 } 370 371 @Override 372 public void fireOnStateUpdated(DownloadRequest request, FileInfo fileInfo, int state) { 373 if (mDownloadStatusListener == null) { 374 return; 375 } 376 mHandler.post(() -> mDownloadStatusListener.onStatusUpdated(request, fileInfo, state)); 377 } 378 379 @Override 380 public void actuallyStartDownloadFlow() { 381 DownloadRequest request = mReceivedRequests.get(0); 382 List<FileInfo> requestedFiles = getRequestedFiles(request); 383 // Compose the FILE_DESCRIPTOR_REQUEST_INTENT to get some FDs to write to 384 Intent requestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST); 385 requestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, request.getFileServiceId()); 386 387 requestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, requestedFiles.size()); 388 requestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, mTempFileRootDirPath); 389 requestIntent.setComponent(CTS_TEST_RECEIVER_COMPONENT); 390 391 // Send as an ordered broadcast, using a BroadcastReceiver to capture the result 392 // containing UriPathPairs. 393 logd("Sending fd-request broadcast"); 394 sendOrderedBroadcast(requestIntent, 395 null, // receiverPermission 396 new BroadcastReceiver() { 397 @Override 398 public void onReceive(Context context, Intent intent) { 399 logd("Got file-descriptors"); 400 Bundle extras = getResultExtras(false); 401 List<UriPathPair> tempFiles = extras.getParcelableArrayList( 402 VendorUtils.EXTRA_FREE_URI_LIST); 403 404 for (int i = 0; i < tempFiles.size(); i++) { 405 UriPathPair tempFile = tempFiles.get(i); 406 FileInfo requestedFile = requestedFiles.get(i); 407 int result = writeContentsToTempFile(tempFile); 408 409 Intent downloadResultIntent = composeDownloadResultIntent( 410 tempFile, request, result, requestedFile); 411 412 logd("Sending broadcast to app: " 413 + downloadResultIntent.toString()); 414 sendOrderedBroadcast(downloadResultIntent, 415 null, // receiverPermission 416 new BroadcastReceiver() { 417 @Override 418 public void onReceive(Context context, Intent intent) { 419 Bundle b = new Bundle(); 420 b.putString(METHOD_NAME, 421 METHOD_DOWNLOAD_RESULT_ACK); 422 b.putInt(ARGUMENT_RESULT_CODE, getResultCode()); 423 mReceivedCalls.add(b); 424 } 425 }, 426 null, // scheduler 427 Activity.RESULT_OK, 428 null, // initialData 429 null /* initialExtras */); 430 } 431 } 432 }, 433 mHandler, // scheduler 434 Activity.RESULT_OK, 435 null, // initialData 436 null /* initialExtras */); 437 438 } 439 }; 440 getRequestedFiles(DownloadRequest request)441 private List<FileInfo> getRequestedFiles(DownloadRequest request) { 442 if (SOURCE_URI_1.equals(request.getSourceUri())) { 443 return Collections.singletonList(FILE_INFO_1); 444 } 445 if (SOURCE_URI_2.equals(request.getSourceUri())) { 446 return Collections.singletonList(FILE_INFO_2); 447 } 448 if (SOURCE_URI_3.equals(request.getSourceUri())) { 449 return FILE_SERVICE_INFO.getFiles(); 450 } 451 return Collections.emptyList(); 452 } 453 composeDownloadResultIntent(UriPathPair tempFile, DownloadRequest request, int result, FileInfo downloadedFile)454 private Intent composeDownloadResultIntent(UriPathPair tempFile, DownloadRequest request, 455 int result, FileInfo downloadedFile) { 456 Intent downloadResultIntent = 457 new Intent(VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL); 458 downloadResultIntent.putExtra( 459 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); 460 downloadResultIntent.putExtra(VendorUtils.EXTRA_FINAL_URI, 461 tempFile.getFilePathUri()); 462 downloadResultIntent.putExtra( 463 MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, downloadedFile); 464 downloadResultIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, 465 mTempFileRootDirPath); 466 downloadResultIntent.putExtra( 467 MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); 468 downloadResultIntent.setComponent(CTS_TEST_RECEIVER_COMPONENT); 469 return downloadResultIntent; 470 } 471 writeContentsToTempFile(UriPathPair tempFile)472 private int writeContentsToTempFile(UriPathPair tempFile) { 473 int result = MbmsDownloadSession.RESULT_SUCCESSFUL; 474 try { 475 ParcelFileDescriptor tempFileFd = 476 getContentResolver().openFileDescriptor( 477 tempFile.getContentUri(), "rw"); 478 OutputStream destinationStream = 479 new ParcelFileDescriptor.AutoCloseOutputStream(tempFileFd); 480 481 destinationStream.write(SAMPLE_FILE_DATA); 482 destinationStream.flush(); 483 } catch (IOException e) { 484 result = MbmsDownloadSession.RESULT_CANCELLED; 485 } 486 return result; 487 } 488 489 @Override onDestroy()490 public void onDestroy() { 491 super.onCreate(); 492 mHandlerThread.quitSafely(); 493 logd("CtsDownloadService onDestroy"); 494 } 495 496 @Override onBind(Intent intent)497 public IBinder onBind(Intent intent) { 498 if (CONTROL_INTERFACE_ACTION.equals(intent.getAction())) { 499 logd("CtsDownloadService control interface bind"); 500 return mControlInterface; 501 } 502 503 logd("CtsDownloadService onBind"); 504 if (mHandlerThread != null && mHandlerThread.isAlive()) { 505 return mDownloadServiceImpl; 506 } 507 508 mHandlerThread = new HandlerThread("CtsDownloadServiceWorker"); 509 mHandlerThread.start(); 510 mHandler = new Handler(mHandlerThread.getLooper()); 511 return mDownloadServiceImpl; 512 } 513 logd(String s)514 private static void logd(String s) { 515 Log.d(TAG, s); 516 } 517 checkInitialized()518 private void checkInitialized() { 519 if (mAppCallback == null) { 520 throw new IllegalStateException("Not yet initialized"); 521 } 522 } 523 } 524