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