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 com.android.phone.testapps.embmsmw; 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.IMbmsDownloadService; 43 import android.telephony.mbms.vendor.MbmsDownloadServiceBase; 44 import android.telephony.mbms.vendor.VendorUtils; 45 import android.util.Log; 46 47 import java.io.IOException; 48 import java.io.InputStream; 49 import java.io.OutputStream; 50 import java.util.ArrayList; 51 import java.util.Arrays; 52 import java.util.Collections; 53 import java.util.HashMap; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 import java.util.concurrent.ConcurrentHashMap; 59 60 public class EmbmsSampleDownloadService extends Service { 61 private static final Set<String> ALLOWED_PACKAGES = new HashSet<String>() {{ 62 add("com.android.phone.testapps.embmsdownload"); 63 }}; 64 65 private static final String LOG_TAG = "EmbmsSampleDownload"; 66 private static final long INITIALIZATION_DELAY = 200; 67 private static final long SEND_FILE_SERVICE_INFO_DELAY = 500; 68 private static final long DOWNLOAD_DELAY_MS = 1000; 69 private static final long FILE_SEPARATION_DELAY = 500; 70 71 private final IMbmsDownloadService mBinder = new MbmsDownloadServiceBase() { 72 @Override 73 public int initialize(int subId, MbmsDownloadSessionCallback callback) { 74 int packageUid = Binder.getCallingUid(); 75 String[] packageNames = getPackageManager().getPackagesForUid(packageUid); 76 if (packageNames == null) { 77 return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; 78 } 79 boolean isUidAllowed = Arrays.stream(packageNames).anyMatch(ALLOWED_PACKAGES::contains); 80 if (!isUidAllowed) { 81 return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; 82 } 83 84 // Do initialization with a bit of a delay to simulate work being done. 85 mHandler.postDelayed(() -> { 86 FrontendAppIdentifier appKey = new FrontendAppIdentifier(packageUid, subId); 87 if (!mAppCallbacks.containsKey(appKey)) { 88 mAppCallbacks.put(appKey, callback); 89 ComponentName appReceiver = VendorUtils.getAppReceiverFromPackageName( 90 EmbmsSampleDownloadService.this, 91 getPackageManager().getNameForUid(packageUid)); 92 mAppReceivers.put(appKey, appReceiver); 93 } else { 94 callback.onError( 95 MbmsErrors.InitializationErrors.ERROR_DUPLICATE_INITIALIZE, ""); 96 return; 97 } 98 callback.onMiddlewareReady(); 99 }, INITIALIZATION_DELAY); 100 101 return MbmsErrors.SUCCESS; 102 } 103 104 @Override 105 public int requestUpdateFileServices(int subscriptionId, 106 List<String> serviceClasses) throws RemoteException { 107 FrontendAppIdentifier appKey = 108 new FrontendAppIdentifier(Binder.getCallingUid(), subscriptionId); 109 checkInitialized(appKey); 110 111 List<FileServiceInfo> serviceInfos = 112 FileServiceRepository.getInstance(EmbmsSampleDownloadService.this) 113 .getFileServicesForClasses(serviceClasses); 114 115 mHandler.postDelayed(() -> { 116 MbmsDownloadSessionCallback appCallback = mAppCallbacks.get(appKey); 117 appCallback.onFileServicesUpdated(serviceInfos); 118 }, SEND_FILE_SERVICE_INFO_DELAY); 119 return MbmsErrors.SUCCESS; 120 } 121 122 @Override 123 public int setTempFileRootDirectory(int subscriptionId, 124 String rootDirectoryPath) throws RemoteException { 125 FrontendAppIdentifier appKey = 126 new FrontendAppIdentifier(Binder.getCallingUid(), subscriptionId); 127 checkInitialized(appKey); 128 129 if (mActiveDownloadRequests.getOrDefault(appKey, Collections.emptySet()).size() > 0) { 130 return MbmsErrors.DownloadErrors.ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT; 131 } 132 mAppTempFileRoots.put(appKey, rootDirectoryPath); 133 return MbmsErrors.SUCCESS; 134 } 135 136 @Override 137 public int download(DownloadRequest downloadRequest) { 138 FrontendAppIdentifier appKey = new FrontendAppIdentifier( 139 Binder.getCallingUid(), downloadRequest.getSubscriptionId()); 140 checkInitialized(appKey); 141 142 mHandler.post(() -> sendFdRequest(downloadRequest, appKey)); 143 return MbmsErrors.SUCCESS; 144 } 145 146 @Override 147 public int addStatusListener(DownloadRequest downloadRequest, 148 DownloadStatusListener callback) throws RemoteException { 149 mDownloadStatusCallbacks.put(downloadRequest, callback); 150 return MbmsErrors.SUCCESS; 151 } 152 153 @Override 154 public int addProgressListener(DownloadRequest downloadRequest, 155 DownloadProgressListener callback) throws RemoteException { 156 mDownloadProgressCallbacks.put(downloadRequest, callback); 157 return MbmsErrors.SUCCESS; 158 } 159 160 @Override 161 public int cancelDownload(DownloadRequest downloadRequest) { 162 FrontendAppIdentifier appKey = new FrontendAppIdentifier( 163 Binder.getCallingUid(), downloadRequest.getSubscriptionId()); 164 checkInitialized(appKey); 165 if (!mActiveDownloadRequests.getOrDefault( 166 appKey, Collections.emptySet()).contains(downloadRequest)) { 167 return MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST; 168 } 169 mActiveDownloadRequests.get(appKey).remove(downloadRequest); 170 return MbmsErrors.SUCCESS; 171 } 172 173 @Override 174 public void onAppCallbackDied(int uid, int subscriptionId) { 175 FrontendAppIdentifier appKey = new FrontendAppIdentifier(uid, subscriptionId); 176 177 Log.i(LOG_TAG, "Disposing app " + appKey + " due to binder death"); 178 mAppCallbacks.remove(appKey); 179 // TODO: call dispose 180 } 181 }; 182 183 private static EmbmsSampleDownloadService sInstance = null; 184 185 private final Map<FrontendAppIdentifier, MbmsDownloadSessionCallback> mAppCallbacks = 186 new HashMap<>(); 187 private final Map<FrontendAppIdentifier, ComponentName> mAppReceivers = new HashMap<>(); 188 private final Map<FrontendAppIdentifier, String> mAppTempFileRoots = new HashMap<>(); 189 private final Map<FrontendAppIdentifier, Set<DownloadRequest>> mActiveDownloadRequests = 190 new ConcurrentHashMap<>(); 191 // A map of app-identifiers to (maps of service-ids to sets of temp file uris in use) 192 private final Map<FrontendAppIdentifier, Map<String, Set<Uri>>> mTempFilesInUse = 193 new ConcurrentHashMap<>(); 194 private final Map<DownloadRequest, DownloadStatusListener> mDownloadStatusCallbacks = 195 new ConcurrentHashMap<>(); 196 private final Map<DownloadRequest, DownloadProgressListener> mDownloadProgressCallbacks = 197 new ConcurrentHashMap<>(); 198 199 private HandlerThread mHandlerThread; 200 private Handler mHandler; 201 private int mDownloadDelayFactor = 1; 202 203 @Override onBind(Intent intent)204 public IBinder onBind(Intent intent) { 205 mHandlerThread = new HandlerThread("EmbmsTestDownloadServiceWorker"); 206 mHandlerThread.start(); 207 mHandler = new Handler(mHandlerThread.getLooper()); 208 sInstance = this; 209 return mBinder.asBinder(); 210 } 211 getInstance()212 public static EmbmsSampleDownloadService getInstance() { 213 return sInstance; 214 } 215 requestCleanup()216 public void requestCleanup() { 217 // Assume that there's only one app, and do it for all the services. 218 FrontendAppIdentifier registeredAppId = mAppReceivers.keySet().iterator().next(); 219 ComponentName appReceiver = mAppReceivers.values().iterator().next(); 220 for (FileServiceInfo fileServiceInfo : 221 FileServiceRepository.getInstance(this).getAllFileServices()) { 222 Intent cleanupIntent = new Intent(VendorUtils.ACTION_CLEANUP); 223 cleanupIntent.setComponent(appReceiver); 224 cleanupIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, fileServiceInfo.getServiceId()); 225 cleanupIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, 226 mAppTempFileRoots.get(registeredAppId)); 227 Set<Uri> tempFilesInUse = 228 mTempFilesInUse.getOrDefault(registeredAppId, Collections.emptyMap()) 229 .getOrDefault(fileServiceInfo.getServiceId(), Collections.emptySet()); 230 cleanupIntent.putExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE, 231 new ArrayList<>(tempFilesInUse)); 232 sendBroadcast(cleanupIntent); 233 } 234 } 235 requestExtraTempFiles(FileServiceInfo serviceInfo)236 public void requestExtraTempFiles(FileServiceInfo serviceInfo) { 237 // Assume one app, and do it for the specified service. 238 FrontendAppIdentifier registeredAppId = mAppReceivers.keySet().iterator().next(); 239 ComponentName appReceiver = mAppReceivers.values().iterator().next(); 240 Intent fdRequestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST); 241 fdRequestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, serviceInfo.getServiceId()); 242 fdRequestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, 10); 243 fdRequestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, 244 mAppTempFileRoots.get(registeredAppId)); 245 fdRequestIntent.setComponent(appReceiver); 246 247 sendOrderedBroadcast(fdRequestIntent, 248 null, // receiverPermission 249 new BroadcastReceiver() { 250 @Override 251 public void onReceive(Context context, Intent intent) { 252 int result = getResultCode(); 253 Bundle extras = getResultExtras(false); 254 Log.i(LOG_TAG, "Received extra temp files. Result " + result); 255 if (extras != null) { 256 Log.i(LOG_TAG, "Got " 257 + extras.getParcelableArrayList( 258 VendorUtils.EXTRA_FREE_URI_LIST).size() 259 + " fds"); 260 } 261 } 262 }, 263 null, // scheduler 264 Activity.RESULT_OK, 265 null, // initialData 266 null /* initialExtras */); 267 } 268 delayDownloads(int factor)269 public void delayDownloads(int factor) { 270 mDownloadDelayFactor = factor; 271 } 272 sendFdRequest(DownloadRequest request, FrontendAppIdentifier appKey)273 private void sendFdRequest(DownloadRequest request, FrontendAppIdentifier appKey) { 274 // Request twice as many as needed to exercise the post-download cleanup mechanism 275 int numFds = getNumFdsNeededForRequest(request) * 2; 276 // Compose the FILE_DESCRIPTOR_REQUEST_INTENT 277 Intent requestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST); 278 requestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, request.getFileServiceId()); 279 requestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, numFds); 280 requestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, 281 mAppTempFileRoots.get(appKey)); 282 requestIntent.setComponent(mAppReceivers.get(appKey)); 283 284 // Send as an ordered broadcast, using a BroadcastReceiver to capture the result 285 // containing UriPathPairs. 286 sendOrderedBroadcast(requestIntent, 287 null, // receiverPermission 288 new BroadcastReceiver() { 289 @Override 290 public void onReceive(Context context, Intent intent) { 291 Bundle resultExtras = getResultExtras(false); 292 // This delay is to emulate the time it'd usually take to fetch the file 293 // off the network. 294 mHandler.postDelayed( 295 () -> performDownload(request, appKey, resultExtras), 296 DOWNLOAD_DELAY_MS); 297 } 298 }, 299 null, // scheduler 300 Activity.RESULT_OK, 301 null, // initialData 302 null /* initialExtras */); 303 } 304 performDownload(DownloadRequest request, FrontendAppIdentifier appKey, Bundle extras)305 private void performDownload(DownloadRequest request, FrontendAppIdentifier appKey, 306 Bundle extras) { 307 List<UriPathPair> tempFiles = extras.getParcelableArrayList( 308 VendorUtils.EXTRA_FREE_URI_LIST); 309 List<FileInfo> filesToDownload = FileServiceRepository.getInstance(this) 310 .getFileServiceInfoForId(request.getFileServiceId()) 311 .getFiles(); 312 313 if (tempFiles.size() != filesToDownload.size() * 2) { 314 Log.w(LOG_TAG, "Incorrect numbers of temp files and files to download..."); 315 } 316 317 if (!mActiveDownloadRequests.containsKey(appKey)) { 318 mActiveDownloadRequests.put(appKey, Collections.synchronizedSet(new HashSet<>())); 319 } 320 mActiveDownloadRequests.get(appKey).add(request); 321 322 // Go through the files one-by-one and send them to the frontend app with a delay between 323 // each one. 324 for (int i = 0; i < tempFiles.size(); i += 2) { 325 if (i >= filesToDownload.size() * 2) { 326 break; 327 } 328 UriPathPair tempFile = tempFiles.get(i); 329 UriPathPair extraTempFile = tempFiles.get(i + 1); 330 addTempFileInUse(appKey, request.getFileServiceId(), 331 tempFile.getFilePathUri()); 332 FileInfo fileToDownload = filesToDownload.get(i / 2); 333 mHandler.postDelayed(() -> { 334 if (mActiveDownloadRequests.get(appKey) == null || 335 !mActiveDownloadRequests.get(appKey).contains(request)) { 336 return; 337 } 338 downloadSingleFile(appKey, request, tempFile, extraTempFile, fileToDownload); 339 removeTempFileInUse(appKey, request.getFileServiceId(), 340 tempFile.getFilePathUri()); 341 }, FILE_SEPARATION_DELAY * i * mDownloadDelayFactor / 2); 342 } 343 } 344 downloadSingleFile(FrontendAppIdentifier appKey, DownloadRequest request, UriPathPair tempFile, UriPathPair extraTempFile, FileInfo fileToDownload)345 private void downloadSingleFile(FrontendAppIdentifier appKey, DownloadRequest request, 346 UriPathPair tempFile, UriPathPair extraTempFile, FileInfo fileToDownload) { 347 int result = MbmsDownloadSession.RESULT_SUCCESSFUL; 348 // Test Callback 349 DownloadStatusListener statusListener = mDownloadStatusCallbacks.get(request); 350 DownloadProgressListener progressListener = mDownloadProgressCallbacks.get(request); 351 if (progressListener != null) { 352 progressListener.onProgressUpdated(request, fileToDownload, 0, 10, 0, 10); 353 } 354 // Test Callback 355 if (statusListener != null) { 356 statusListener.onStatusUpdated(request, fileToDownload, 357 MbmsDownloadSession.STATUS_ACTIVELY_DOWNLOADING); 358 } 359 try { 360 // Get the ParcelFileDescriptor for the single temp file we requested 361 ParcelFileDescriptor tempFileFd = getContentResolver().openFileDescriptor( 362 tempFile.getContentUri(), "rw"); 363 OutputStream destinationStream = 364 new ParcelFileDescriptor.AutoCloseOutputStream(tempFileFd); 365 366 // This is how you get the native fd 367 Log.i(LOG_TAG, "Native fd: " + tempFileFd.getFd()); 368 369 int resourceId = FileServiceRepository.getInstance(this) 370 .getResourceForFileUri(fileToDownload.getUri()); 371 // Open the picture we have in our res/raw directory 372 InputStream image = getResources().openRawResource(resourceId); 373 374 // Copy it into the temp file in the app's file space (crudely) 375 byte[] imageBuffer = new byte[image.available()]; 376 image.read(imageBuffer); 377 destinationStream.write(imageBuffer); 378 destinationStream.flush(); 379 } catch (IOException e) { 380 result = MbmsDownloadSession.RESULT_CANCELLED; 381 } 382 // Test Callback 383 if (progressListener != null) { 384 progressListener.onProgressUpdated(request, fileToDownload, 10, 10, 10, 10); 385 } 386 // Take a round-trip through the download request serialization to exercise it 387 DownloadRequest request1 = DownloadRequest.Builder.fromSerializedRequest( 388 request.toByteArray()).build(); 389 390 Intent downloadResultIntent = 391 new Intent(VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL); 392 downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request1); 393 downloadResultIntent.putExtra(VendorUtils.EXTRA_FINAL_URI, 394 tempFile.getFilePathUri()); 395 downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, fileToDownload); 396 downloadResultIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, 397 mAppTempFileRoots.get(appKey)); 398 ArrayList<Uri> tempFileList = new ArrayList<>(2); 399 tempFileList.add(tempFile.getFilePathUri()); 400 tempFileList.add(extraTempFile.getFilePathUri()); 401 downloadResultIntent.putParcelableArrayListExtra( 402 VendorUtils.EXTRA_TEMP_LIST, tempFileList); 403 downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); 404 downloadResultIntent.setComponent(mAppReceivers.get(appKey)); 405 406 sendOrderedBroadcast(downloadResultIntent, 407 null, // receiverPermission 408 new BroadcastReceiver() { 409 @Override 410 public void onReceive(Context context, Intent intent) { 411 int resultCode = getResultCode(); 412 Log.i(LOG_TAG, "Download result ack: " + resultCode); 413 } 414 }, 415 null, // scheduler 416 Activity.RESULT_OK, 417 null, // initialData 418 null /* initialExtras */); 419 } 420 checkInitialized(FrontendAppIdentifier appKey)421 private void checkInitialized(FrontendAppIdentifier appKey) { 422 if (!mAppCallbacks.containsKey(appKey)) { 423 throw new IllegalStateException("Not yet initialized"); 424 } 425 } 426 getNumFdsNeededForRequest(DownloadRequest request)427 private int getNumFdsNeededForRequest(DownloadRequest request) { 428 return FileServiceRepository.getInstance(this) 429 .getFileServiceInfoForId(request.getFileServiceId()).getFiles().size(); 430 } 431 addTempFileInUse(FrontendAppIdentifier appKey, String serviceId, Uri tempFileUri)432 private void addTempFileInUse(FrontendAppIdentifier appKey, String serviceId, Uri tempFileUri) { 433 Map<String, Set<Uri>> tempFileByService = mTempFilesInUse.get(appKey); 434 if (tempFileByService == null) { 435 tempFileByService = new ConcurrentHashMap<>(); 436 mTempFilesInUse.put(appKey, tempFileByService); 437 } 438 Set<Uri> tempFilesInUse = tempFileByService.get(serviceId); 439 if (tempFilesInUse == null) { 440 tempFilesInUse = ConcurrentHashMap.newKeySet(); 441 tempFileByService.put(serviceId, tempFilesInUse); 442 } 443 tempFilesInUse.add(tempFileUri); 444 } 445 removeTempFileInUse(FrontendAppIdentifier appKey, String serviceId, Uri tempFileUri)446 private void removeTempFileInUse(FrontendAppIdentifier appKey, String serviceId, 447 Uri tempFileUri) { 448 Set<Uri> tempFilesInUse = mTempFilesInUse.getOrDefault(appKey, Collections.emptyMap()) 449 .getOrDefault(serviceId, Collections.emptySet()); 450 if (tempFilesInUse.contains(tempFileUri)) { 451 tempFilesInUse.remove(tempFileUri); 452 } else { 453 Log.w(LOG_TAG, "Trying to remove unknown temp file in use " + tempFileUri + " for app" + 454 appKey + " and service id " + serviceId); 455 } 456 } 457 } 458