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