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