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