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