blob: 093d683d87d12e4ceb39698e8244bbead5d308bd [file] [log] [blame]
/*
* 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 com.android.phone.testapps.embmsmw;
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.IMbmsDownloadService;
import android.telephony.mbms.vendor.MbmsDownloadServiceBase;
import android.telephony.mbms.vendor.VendorUtils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class EmbmsSampleDownloadService extends Service {
private static final Set<String> ALLOWED_PACKAGES = new HashSet<String>() {{
add("com.android.phone.testapps.embmsdownload");
}};
private static final String LOG_TAG = "EmbmsSampleDownload";
private static final long INITIALIZATION_DELAY = 200;
private static final long SEND_FILE_SERVICE_INFO_DELAY = 500;
private static final long DOWNLOAD_DELAY_MS = 1000;
private static final long FILE_SEPARATION_DELAY = 500;
private final IMbmsDownloadService mBinder = new MbmsDownloadServiceBase() {
@Override
public int initialize(int subId, MbmsDownloadSessionCallback callback) {
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;
}
// Do initialization with a bit of a delay to simulate work being done.
mHandler.postDelayed(() -> {
FrontendAppIdentifier appKey = new FrontendAppIdentifier(packageUid, subId);
if (!mAppCallbacks.containsKey(appKey)) {
mAppCallbacks.put(appKey, callback);
ComponentName appReceiver = VendorUtils.getAppReceiverFromPackageName(
EmbmsSampleDownloadService.this,
getPackageManager().getNameForUid(packageUid));
mAppReceivers.put(appKey, appReceiver);
} else {
callback.onError(
MbmsErrors.InitializationErrors.ERROR_DUPLICATE_INITIALIZE, "");
return;
}
callback.onMiddlewareReady();
}, INITIALIZATION_DELAY);
return MbmsErrors.SUCCESS;
}
@Override
public int requestUpdateFileServices(int subscriptionId,
List<String> serviceClasses) throws RemoteException {
FrontendAppIdentifier appKey =
new FrontendAppIdentifier(Binder.getCallingUid(), subscriptionId);
checkInitialized(appKey);
List<FileServiceInfo> serviceInfos =
FileServiceRepository.getInstance(EmbmsSampleDownloadService.this)
.getFileServicesForClasses(serviceClasses);
mHandler.postDelayed(() -> {
MbmsDownloadSessionCallback appCallback = mAppCallbacks.get(appKey);
appCallback.onFileServicesUpdated(serviceInfos);
}, SEND_FILE_SERVICE_INFO_DELAY);
return MbmsErrors.SUCCESS;
}
@Override
public int setTempFileRootDirectory(int subscriptionId,
String rootDirectoryPath) throws RemoteException {
FrontendAppIdentifier appKey =
new FrontendAppIdentifier(Binder.getCallingUid(), subscriptionId);
checkInitialized(appKey);
if (mActiveDownloadRequests.getOrDefault(appKey, Collections.emptySet()).size() > 0) {
return MbmsErrors.DownloadErrors.ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT;
}
mAppTempFileRoots.put(appKey, rootDirectoryPath);
return MbmsErrors.SUCCESS;
}
@Override
public int download(DownloadRequest downloadRequest) {
FrontendAppIdentifier appKey = new FrontendAppIdentifier(
Binder.getCallingUid(), downloadRequest.getSubscriptionId());
checkInitialized(appKey);
mHandler.post(() -> sendFdRequest(downloadRequest, appKey));
return MbmsErrors.SUCCESS;
}
@Override
public int registerStateCallback(DownloadRequest downloadRequest,
DownloadStateCallback callback) throws RemoteException {
mDownloadStateCallbacks.put(downloadRequest, callback);
return MbmsErrors.SUCCESS;
}
@Override
public int cancelDownload(DownloadRequest downloadRequest) {
FrontendAppIdentifier appKey = new FrontendAppIdentifier(
Binder.getCallingUid(), downloadRequest.getSubscriptionId());
checkInitialized(appKey);
if (!mActiveDownloadRequests.getOrDefault(
appKey, Collections.emptySet()).contains(downloadRequest)) {
return MbmsErrors.DownloadErrors.ERROR_UNKNOWN_DOWNLOAD_REQUEST;
}
mActiveDownloadRequests.get(appKey).remove(downloadRequest);
return MbmsErrors.SUCCESS;
}
@Override
public void onAppCallbackDied(int uid, int subscriptionId) {
FrontendAppIdentifier appKey = new FrontendAppIdentifier(uid, subscriptionId);
Log.i(LOG_TAG, "Disposing app " + appKey + " due to binder death");
mAppCallbacks.remove(appKey);
// TODO: call dispose
}
};
private static EmbmsSampleDownloadService sInstance = null;
private final Map<FrontendAppIdentifier, MbmsDownloadSessionCallback> mAppCallbacks =
new HashMap<>();
private final Map<FrontendAppIdentifier, ComponentName> mAppReceivers = new HashMap<>();
private final Map<FrontendAppIdentifier, String> mAppTempFileRoots = new HashMap<>();
private final Map<FrontendAppIdentifier, Set<DownloadRequest>> mActiveDownloadRequests =
new ConcurrentHashMap<>();
// A map of app-identifiers to (maps of service-ids to sets of temp file uris in use)
private final Map<FrontendAppIdentifier, Map<String, Set<Uri>>> mTempFilesInUse =
new ConcurrentHashMap<>();
private final Map<DownloadRequest, DownloadStateCallback> mDownloadStateCallbacks =
new ConcurrentHashMap<>();
private HandlerThread mHandlerThread;
private Handler mHandler;
private int mDownloadDelayFactor = 1;
@Override
public IBinder onBind(Intent intent) {
mHandlerThread = new HandlerThread("EmbmsTestDownloadServiceWorker");
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
sInstance = this;
return mBinder.asBinder();
}
public static EmbmsSampleDownloadService getInstance() {
return sInstance;
}
public void requestCleanup() {
// Assume that there's only one app, and do it for all the services.
FrontendAppIdentifier registeredAppId = mAppReceivers.keySet().iterator().next();
ComponentName appReceiver = mAppReceivers.values().iterator().next();
for (FileServiceInfo fileServiceInfo :
FileServiceRepository.getInstance(this).getAllFileServices()) {
Intent cleanupIntent = new Intent(VendorUtils.ACTION_CLEANUP);
cleanupIntent.setComponent(appReceiver);
cleanupIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, fileServiceInfo.getServiceId());
cleanupIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT,
mAppTempFileRoots.get(registeredAppId));
Set<Uri> tempFilesInUse =
mTempFilesInUse.getOrDefault(registeredAppId, Collections.emptyMap())
.getOrDefault(fileServiceInfo.getServiceId(), Collections.emptySet());
cleanupIntent.putExtra(VendorUtils.EXTRA_TEMP_FILES_IN_USE,
new ArrayList<>(tempFilesInUse));
sendBroadcast(cleanupIntent);
}
}
public void requestExtraTempFiles(FileServiceInfo serviceInfo) {
// Assume one app, and do it for the specified service.
FrontendAppIdentifier registeredAppId = mAppReceivers.keySet().iterator().next();
ComponentName appReceiver = mAppReceivers.values().iterator().next();
Intent fdRequestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST);
fdRequestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, serviceInfo.getServiceId());
fdRequestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, 10);
fdRequestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT,
mAppTempFileRoots.get(registeredAppId));
fdRequestIntent.setComponent(appReceiver);
sendOrderedBroadcast(fdRequestIntent,
null, // receiverPermission
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int result = getResultCode();
Bundle extras = getResultExtras(false);
Log.i(LOG_TAG, "Received extra temp files. Result " + result);
if (extras != null) {
Log.i(LOG_TAG, "Got "
+ extras.getParcelableArrayList(
VendorUtils.EXTRA_FREE_URI_LIST).size()
+ " fds");
}
}
},
null, // scheduler
Activity.RESULT_OK,
null, // initialData
null /* initialExtras */);
}
public void delayDownloads(int factor) {
mDownloadDelayFactor = factor;
}
private void sendFdRequest(DownloadRequest request, FrontendAppIdentifier appKey) {
int numFds = getNumFdsNeededForRequest(request);
// Compose the FILE_DESCRIPTOR_REQUEST_INTENT
Intent requestIntent = new Intent(VendorUtils.ACTION_FILE_DESCRIPTOR_REQUEST);
requestIntent.putExtra(VendorUtils.EXTRA_SERVICE_ID, request.getFileServiceId());
requestIntent.putExtra(VendorUtils.EXTRA_FD_COUNT, numFds);
requestIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT,
mAppTempFileRoots.get(appKey));
requestIntent.setComponent(mAppReceivers.get(appKey));
// Send as an ordered broadcast, using a BroadcastReceiver to capture the result
// containing UriPathPairs.
sendOrderedBroadcast(requestIntent,
null, // receiverPermission
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle resultExtras = getResultExtras(false);
// This delay is to emulate the time it'd usually take to fetch the file
// off the network.
mHandler.postDelayed(
() -> performDownload(request, appKey, resultExtras),
DOWNLOAD_DELAY_MS);
}
},
null, // scheduler
Activity.RESULT_OK,
null, // initialData
null /* initialExtras */);
}
private void performDownload(DownloadRequest request, FrontendAppIdentifier appKey,
Bundle extras) {
List<UriPathPair> tempFiles = extras.getParcelableArrayList(
VendorUtils.EXTRA_FREE_URI_LIST);
List<FileInfo> filesToDownload = FileServiceRepository.getInstance(this)
.getFileServiceInfoForId(request.getFileServiceId())
.getFiles();
if (tempFiles.size() != filesToDownload.size()) {
Log.w(LOG_TAG, "Different numbers of temp files and files to download...");
}
if (!mActiveDownloadRequests.containsKey(appKey)) {
mActiveDownloadRequests.put(appKey, Collections.synchronizedSet(new HashSet<>()));
}
mActiveDownloadRequests.get(appKey).add(request);
// Go through the files one-by-one and send them to the frontend app with a delay between
// each one.
for (int i = 0; i < tempFiles.size(); i++) {
if (i >= filesToDownload.size()) {
break;
}
UriPathPair tempFile = tempFiles.get(i);
addTempFileInUse(appKey, request.getFileServiceId(),
tempFile.getFilePathUri());
FileInfo fileToDownload = filesToDownload.get(i);
mHandler.postDelayed(() -> {
if (mActiveDownloadRequests.get(appKey) == null ||
!mActiveDownloadRequests.get(appKey).contains(request)) {
return;
}
downloadSingleFile(appKey, request, tempFile, fileToDownload);
removeTempFileInUse(appKey, request.getFileServiceId(),
tempFile.getFilePathUri());
}, FILE_SEPARATION_DELAY * i * mDownloadDelayFactor);
}
}
private void downloadSingleFile(FrontendAppIdentifier appKey, DownloadRequest request,
UriPathPair tempFile, FileInfo fileToDownload) {
int result = MbmsDownloadSession.RESULT_SUCCESSFUL;
// Test Callback
DownloadStateCallback c = mDownloadStateCallbacks.get(request);
if (c != null) {
c.onProgressUpdated(request, fileToDownload, 0, 10, 0, 10);
}
// Test Callback
if (c != null) {
c.onStateUpdated(request, fileToDownload,
MbmsDownloadSession.STATUS_ACTIVELY_DOWNLOADING);
}
try {
// Get the ParcelFileDescriptor for the single temp file we requested
ParcelFileDescriptor tempFileFd = getContentResolver().openFileDescriptor(
tempFile.getContentUri(), "rw");
OutputStream destinationStream =
new ParcelFileDescriptor.AutoCloseOutputStream(tempFileFd);
// This is how you get the native fd
Log.i(LOG_TAG, "Native fd: " + tempFileFd.getFd());
int resourceId = FileServiceRepository.getInstance(this)
.getResourceForFileUri(fileToDownload.getUri());
// Open the picture we have in our res/raw directory
InputStream image = getResources().openRawResource(resourceId);
// Copy it into the temp file in the app's file space (crudely)
byte[] imageBuffer = new byte[image.available()];
image.read(imageBuffer);
destinationStream.write(imageBuffer);
destinationStream.flush();
} catch (IOException e) {
result = MbmsDownloadSession.RESULT_CANCELLED;
}
// Test Callback
if (c != null) {
c.onProgressUpdated(request, fileToDownload, 10, 10, 10, 10);
}
// Take a round-trip through the download request serialization to exercise it
DownloadRequest request1 = new DownloadRequest.Builder()
.setSource(request.getSourceUri())
.setSubscriptionId(request.getSubscriptionId())
.setServiceId(request.getFileServiceId())
.setOpaqueData(request.getOpaqueData())
.build();
Intent downloadResultIntent =
new Intent(VendorUtils.ACTION_DOWNLOAD_RESULT_INTERNAL);
downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_REQUEST, request1);
downloadResultIntent.putExtra(VendorUtils.EXTRA_FINAL_URI,
tempFile.getFilePathUri());
downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_FILE_INFO, fileToDownload);
downloadResultIntent.putExtra(VendorUtils.EXTRA_TEMP_FILE_ROOT,
mAppTempFileRoots.get(appKey));
ArrayList<Uri> tempFileList = new ArrayList<>(1);
tempFileList.add(tempFile.getFilePathUri());
downloadResultIntent.getExtras().putParcelableArrayList(
VendorUtils.EXTRA_TEMP_LIST, tempFileList);
downloadResultIntent.putExtra(MbmsDownloadSession.EXTRA_MBMS_DOWNLOAD_RESULT, result);
downloadResultIntent.setComponent(mAppReceivers.get(appKey));
sendOrderedBroadcast(downloadResultIntent,
null, // receiverPermission
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int resultCode = getResultCode();
Log.i(LOG_TAG, "Download result ack: " + resultCode);
}
},
null, // scheduler
Activity.RESULT_OK,
null, // initialData
null /* initialExtras */);
}
private void checkInitialized(FrontendAppIdentifier appKey) {
if (!mAppCallbacks.containsKey(appKey)) {
throw new IllegalStateException("Not yet initialized");
}
}
private int getNumFdsNeededForRequest(DownloadRequest request) {
return FileServiceRepository.getInstance(this)
.getFileServiceInfoForId(request.getFileServiceId()).getFiles().size();
}
private void addTempFileInUse(FrontendAppIdentifier appKey, String serviceId, Uri tempFileUri) {
Map<String, Set<Uri>> tempFileByService = mTempFilesInUse.get(appKey);
if (tempFileByService == null) {
tempFileByService = new ConcurrentHashMap<>();
mTempFilesInUse.put(appKey, tempFileByService);
}
Set<Uri> tempFilesInUse = tempFileByService.get(serviceId);
if (tempFilesInUse == null) {
tempFilesInUse = ConcurrentHashMap.newKeySet();
tempFileByService.put(serviceId, tempFilesInUse);
}
tempFilesInUse.add(tempFileUri);
}
private void removeTempFileInUse(FrontendAppIdentifier appKey, String serviceId,
Uri tempFileUri) {
Set<Uri> tempFilesInUse = mTempFilesInUse.getOrDefault(appKey, Collections.emptyMap())
.getOrDefault(serviceId, Collections.emptySet());
if (tempFilesInUse.contains(tempFileUri)) {
tempFilesInUse.remove(tempFileUri);
} else {
Log.w(LOG_TAG, "Trying to remove unknown temp file in use " + tempFileUri + " for app" +
appKey + " and service id " + serviceId);
}
}
}