| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package android.telephony.cts.embmstestapp; |
| |
| import android.app.Activity; |
| import android.app.Service; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.telephony.MbmsDownloadSession; |
| import android.telephony.mbms.DownloadRequest; |
| import android.telephony.mbms.DownloadStateCallback; |
| import android.telephony.mbms.FileInfo; |
| import android.telephony.mbms.FileServiceInfo; |
| import android.telephony.mbms.MbmsDownloadSessionCallback; |
| import android.telephony.mbms.MbmsErrors; |
| import android.telephony.mbms.UriPathPair; |
| import android.telephony.mbms.vendor.MbmsDownloadServiceBase; |
| import android.telephony.mbms.vendor.VendorUtils; |
| import android.util.Log; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| public class CtsDownloadService extends Service { |
| private static final Set<String> ALLOWED_PACKAGES = new HashSet<String>() {{ |
| add("android.telephony.cts"); |
| }}; |
| private static final String TAG = "EmbmsTestDownload"; |
| |
| public static final String METHOD_NAME = "method_name"; |
| public static final String METHOD_INITIALIZE = "initialize"; |
| public static final String METHOD_REQUEST_UPDATE_FILE_SERVICES = |
| "requestUpdateFileServices"; |
| public static final String METHOD_SET_TEMP_FILE_ROOT = "setTempFileRootDirectory"; |
| public static final String METHOD_RESET_DOWNLOAD_KNOWLEDGE = "resetDownloadKnowledge"; |
| public static final String METHOD_GET_DOWNLOAD_STATUS = "getDownloadStatus"; |
| public static final String METHOD_CANCEL_DOWNLOAD = "cancelDownload"; |
| public static final String METHOD_CLOSE = "close"; |
| // Not a method call, but it's a form of communication to the middleware so it's included |
| // here for convenience. |
| public static final String METHOD_DOWNLOAD_RESULT_ACK = "downloadResultAck"; |
| |
| public static final String ARGUMENT_SUBSCRIPTION_ID = "subscriptionId"; |
| public static final String ARGUMENT_SERVICE_CLASSES = "serviceClasses"; |
| public static final String ARGUMENT_ROOT_DIRECTORY_PATH = "rootDirectoryPath"; |
| public static final String ARGUMENT_DOWNLOAD_REQUEST = "downloadRequest"; |
| public static final String ARGUMENT_FILE_INFO = "fileInfo"; |
| public static final String ARGUMENT_RESULT_CODE = "resultCode"; |
| |
| public static final String CONTROL_INTERFACE_ACTION = |
| "android.telephony.cts.embmstestapp.ACTION_CONTROL_MIDDLEWARE"; |
| public static final ComponentName CONTROL_INTERFACE_COMPONENT = |
| ComponentName.unflattenFromString( |
| "android.telephony.cts.embmstestapp/.CtsDownloadService"); |
| public static final ComponentName CTS_TEST_RECEIVER_COMPONENT = |
| ComponentName.unflattenFromString( |
| "android.telephony.cts/android.telephony.mbms.MbmsDownloadReceiver"); |
| |
| public static final Uri DOWNLOAD_SOURCE_URI = Uri.parse("http://www.example.com/file_download"); |
| public static final FileServiceInfo FILE_SERVICE_INFO; |
| public static final FileInfo FILE_INFO = new FileInfo( |
| DOWNLOAD_SOURCE_URI.buildUpon().appendPath("file1.txt").build(), |
| "text/plain"); |
| public static final byte[] SAMPLE_FILE_DATA = "this is some sample file data".getBytes(); |
| |
| static { |
| String id = "FileServiceId"; |
| Map<Locale, String> localeDict = new HashMap<Locale, String>() {{ |
| put(Locale.US, "Entertainment Source 1"); |
| put(Locale.CANADA, "Entertainment Source 1, eh?"); |
| }}; |
| List<Locale> locales = new ArrayList<Locale>() {{ |
| add(Locale.CANADA); |
| add(Locale.US); |
| }}; |
| FILE_SERVICE_INFO = new FileServiceInfo(localeDict, "class1", locales, |
| id, new Date(2017, 8, 21, 18, 20, 29), |
| new Date(2017, 8, 21, 18, 23, 9), Collections.singletonList(FILE_INFO)); |
| } |
| |
| private MbmsDownloadSessionCallback mAppCallback; |
| private DownloadStateCallback mDownloadStateCallback; |
| |
| private HandlerThread mHandlerThread; |
| private Handler mHandler; |
| private List<Bundle> mReceivedCalls = new LinkedList<>(); |
| private int mErrorCodeOverride = MbmsErrors.SUCCESS; |
| private List<DownloadRequest> mReceivedRequests = new LinkedList<>(); |
| private String mTempFileRootDirPath = null; |
| |
| private final MbmsDownloadServiceBase mDownloadServiceImpl = new MbmsDownloadServiceBase() { |
| @Override |
| public int initialize(int subscriptionId, MbmsDownloadSessionCallback callback) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_INITIALIZE); |
| b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); |
| mReceivedCalls.add(b); |
| |
| if (mErrorCodeOverride != MbmsErrors.SUCCESS) { |
| return mErrorCodeOverride; |
| } |
| |
| int packageUid = Binder.getCallingUid(); |
| String[] packageNames = getPackageManager().getPackagesForUid(packageUid); |
| if (packageNames == null) { |
| return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; |
| } |
| boolean isUidAllowed = Arrays.stream(packageNames).anyMatch(ALLOWED_PACKAGES::contains); |
| if (!isUidAllowed) { |
| return MbmsErrors.InitializationErrors.ERROR_APP_PERMISSIONS_NOT_GRANTED; |
| } |
| |
| mHandler.post(() -> { |
| if (mAppCallback == null) { |
| mAppCallback = callback; |
| } else { |
| callback.onError( |
| MbmsErrors.InitializationErrors.ERROR_DUPLICATE_INITIALIZE, ""); |
| return; |
| } |
| callback.onMiddlewareReady(); |
| }); |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public int requestUpdateFileServices(int subscriptionId, List<String> serviceClasses) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_REQUEST_UPDATE_FILE_SERVICES); |
| b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); |
| b.putStringArrayList(ARGUMENT_SERVICE_CLASSES, new ArrayList<>(serviceClasses)); |
| mReceivedCalls.add(b); |
| |
| if (mErrorCodeOverride != MbmsErrors.SUCCESS) { |
| return mErrorCodeOverride; |
| } |
| |
| List<FileServiceInfo> serviceInfos = Collections.singletonList(FILE_SERVICE_INFO); |
| |
| mHandler.post(() -> { |
| if (mAppCallback!= null) { |
| mAppCallback.onFileServicesUpdated(serviceInfos); |
| } |
| }); |
| |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public int download(DownloadRequest downloadRequest) { |
| mReceivedRequests.add(downloadRequest); |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public int setTempFileRootDirectory(int subscriptionId, String rootDirectoryPath) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_SET_TEMP_FILE_ROOT); |
| b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); |
| b.putString(ARGUMENT_ROOT_DIRECTORY_PATH, rootDirectoryPath); |
| mReceivedCalls.add(b); |
| mTempFileRootDirPath = rootDirectoryPath; |
| return 0; |
| } |
| |
| @Override |
| public int registerStateCallback(DownloadRequest downloadRequest, |
| DownloadStateCallback listener) throws RemoteException { |
| mDownloadStateCallback = listener; |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public void dispose(int subscriptionId) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_CLOSE); |
| b.putInt(ARGUMENT_SUBSCRIPTION_ID, subscriptionId); |
| mReceivedCalls.add(b); |
| } |
| |
| @Override |
| public int getDownloadStatus(DownloadRequest downloadRequest, FileInfo fileInfo) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_GET_DOWNLOAD_STATUS); |
| b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest); |
| b.putParcelable(ARGUMENT_FILE_INFO, fileInfo); |
| mReceivedCalls.add(b); |
| return MbmsDownloadSession.STATUS_ACTIVELY_DOWNLOADING; |
| } |
| |
| @Override |
| public int cancelDownload(DownloadRequest request) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_CANCEL_DOWNLOAD); |
| b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, request); |
| mReceivedCalls.add(b); |
| mReceivedRequests.remove(request); |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public List<DownloadRequest> listPendingDownloads(int subscriptionId) { |
| return mReceivedRequests; |
| } |
| |
| @Override |
| public int unregisterStateCallback(DownloadRequest downloadRequest, |
| DownloadStateCallback callback) { |
| mDownloadStateCallback = null; |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public int resetDownloadKnowledge(DownloadRequest downloadRequest) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_RESET_DOWNLOAD_KNOWLEDGE); |
| b.putParcelable(ARGUMENT_DOWNLOAD_REQUEST, downloadRequest); |
| mReceivedCalls.add(b); |
| return MbmsErrors.SUCCESS; |
| } |
| |
| @Override |
| public void onAppCallbackDied(int uid, int subscriptionId) { |
| mAppCallback = null; |
| } |
| }; |
| |
| private final IBinder mControlInterface = new ICtsDownloadMiddlewareControl.Stub() { |
| @Override |
| public void reset() { |
| mReceivedCalls.clear(); |
| mHandler.removeCallbacksAndMessages(null); |
| mAppCallback = null; |
| mErrorCodeOverride = MbmsErrors.SUCCESS; |
| mReceivedRequests.clear(); |
| mDownloadStateCallback = null; |
| mTempFileRootDirPath = null; |
| } |
| |
| @Override |
| public List<Bundle> getDownloadSessionCalls() { |
| return mReceivedCalls; |
| } |
| |
| @Override |
| public void forceErrorCode(int error) { |
| mErrorCodeOverride = error; |
| } |
| |
| @Override |
| public void fireErrorOnSession(int errorCode, String message) { |
| mHandler.post(() -> mAppCallback.onError(errorCode, message)); |
| } |
| |
| @Override |
| public void fireOnProgressUpdated(DownloadRequest request, FileInfo fileInfo, |
| int currentDownloadSize, int fullDownloadSize, |
| int currentDecodedSize, int fullDecodedSize) { |
| if (mDownloadStateCallback == null) { |
| return; |
| } |
| mHandler.post(() -> mDownloadStateCallback.onProgressUpdated(request, fileInfo, |
| currentDownloadSize, fullDownloadSize, currentDecodedSize, fullDecodedSize)); |
| } |
| |
| @Override |
| public void fireOnStateUpdated(DownloadRequest request, FileInfo fileInfo, int state) { |
| if (mDownloadStateCallback == null) { |
| return; |
| } |
| mHandler.post(() -> mDownloadStateCallback.onStateUpdated(request, fileInfo, state)); |
| } |
| |
| @Override |
| public void actuallyStartDownloadFlow() { |
| DownloadRequest request = mReceivedRequests.get(0); |
| // Compose the FILE_DESCRIPTOR_REQUEST_INTENT to get some FDs to write to |
| Intent requestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST); |
| requestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, request.getFileServiceId()); |
| requestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, 1); |
| requestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, mTempFileRootDirPath); |
| requestIntent.setComponent(CTS_TEST_RECEIVER_COMPONENT); |
| |
| // Send as an ordered broadcast, using a BroadcastReceiver to capture the result |
| // containing UriPathPairs. |
| logd("Sending fd-request broadcast"); |
| sendOrderedBroadcast(requestIntent, |
| null, // receiverPermission |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| logd("Got file-descriptors"); |
| Bundle extras = getResultExtras(false); |
| UriPathPair tempFile = (UriPathPair) extras.getParcelableArrayList( |
| VendorUtils.EXTRA_FREE_URI_LIST).get(0); |
| int result = MbmsDownloadSession.RESULT_SUCCESSFUL; |
| try { |
| ParcelFileDescriptor tempFileFd = |
| getContentResolver().openFileDescriptor( |
| tempFile.getContentUri(), "rw"); |
| OutputStream destinationStream = |
| new ParcelFileDescriptor.AutoCloseOutputStream(tempFileFd); |
| |
| destinationStream.write(SAMPLE_FILE_DATA); |
| destinationStream.flush(); |
| } catch (IOException e) { |
| result = MbmsDownloadSession.RESULT_CANCELLED; |
| } |
| |
| Intent downloadResultIntent = |
| new Intent(VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL); |
| downloadResultIntent.putExtra( |
| MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request); |
| downloadResultIntent.putExtra(VendorUtils.EXTRA_FINAL_URI, |
| tempFile.getFilePathUri()); |
| downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, |
| FILE_INFO); |
| downloadResultIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT, |
| mTempFileRootDirPath); |
| downloadResultIntent.putExtra( |
| MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result); |
| downloadResultIntent.setComponent(CTS_TEST_RECEIVER_COMPONENT); |
| |
| logd("Sending broadcast to app: " + downloadResultIntent.toString()); |
| sendOrderedBroadcast(downloadResultIntent, |
| null, // receiverPermission |
| new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Bundle b = new Bundle(); |
| b.putString(METHOD_NAME, METHOD_DOWNLOAD_RESULT_ACK); |
| b.putInt(ARGUMENT_RESULT_CODE, getResultCode()); |
| mReceivedCalls.add(b); |
| } |
| }, |
| null, // scheduler |
| Activity.RESULT_OK, |
| null, // initialData |
| null /* initialExtras */); |
| } |
| }, |
| mHandler, // scheduler |
| Activity.RESULT_OK, |
| null, // initialData |
| null /* initialExtras */); |
| |
| } |
| }; |
| |
| @Override |
| public void onDestroy() { |
| super.onCreate(); |
| mHandlerThread.quitSafely(); |
| logd("CtsDownloadService onDestroy"); |
| } |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| if (CONTROL_INTERFACE_ACTION.equals(intent.getAction())) { |
| logd("CtsDownloadService control interface bind"); |
| return mControlInterface; |
| } |
| |
| logd("CtsDownloadService onBind"); |
| if (mHandlerThread != null && mHandlerThread.isAlive()) { |
| return mDownloadServiceImpl; |
| } |
| |
| mHandlerThread = new HandlerThread("CtsDownloadServiceWorker"); |
| mHandlerThread.start(); |
| mHandler = new Handler(mHandlerThread.getLooper()); |
| return mDownloadServiceImpl; |
| } |
| |
| private static void logd(String s) { |
| Log.d(TAG, s); |
| } |
| |
| private void checkInitialized() { |
| if (mAppCallback == null) { |
| throw new IllegalStateException("Not yet initialized"); |
| } |
| } |
| } |