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