diff options
| author | 2017-06-08 01:26:28 +0000 | |
|---|---|---|
| committer | 2017-06-08 01:26:28 +0000 | |
| commit | a807ee8f1c6e55dc5878c738ac76db0f71a9f29b (patch) | |
| tree | fce589a4f406f4ccf71728a0b160ad949fb0911f | |
| parent | 35182218d36f688d646cf5bac603f52bd665c4ab (diff) | |
| parent | da5b77fa9b8baf08208180a7186efae1fdf9fefc (diff) | |
Merge "Embms download setup"
am: da5b77fa9b
Change-Id: I18b378fd0c999e40e3dc8cf9d1248414489880c6
5 files changed, 708 insertions, 43 deletions
diff --git a/telephony/java/android/telephony/MbmsDownloadManager.java b/telephony/java/android/telephony/MbmsDownloadManager.java index a9ec299c4cd8..c8c6d014a059 100644 --- a/telephony/java/android/telephony/MbmsDownloadManager.java +++ b/telephony/java/android/telephony/MbmsDownloadManager.java @@ -16,19 +16,24 @@ package android.telephony; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.net.Uri; +import android.os.IBinder; import android.os.RemoteException; import android.telephony.mbms.IDownloadCallback; import android.telephony.mbms.DownloadRequest; import android.telephony.mbms.DownloadStatus; import android.telephony.mbms.IMbmsDownloadManagerCallback; import android.telephony.mbms.MbmsException; +import android.telephony.mbms.MbmsUtils; import android.telephony.mbms.vendor.IMbmsDownloadService; import android.util.Log; import java.util.List; +import java.util.concurrent.CountDownLatch; import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; @@ -36,6 +41,8 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; public class MbmsDownloadManager { private static final String LOG_TAG = MbmsDownloadManager.class.getSimpleName(); + public static final String MBMS_DOWNLOAD_SERVICE_ACTION = + "android.telephony.action.EmbmsDownload"; /** * The MBMS middleware should send this when a download of single file has completed or * failed. Mandatory extras are @@ -76,15 +83,15 @@ public class MbmsDownloadManager { "android.telephony.mbms.action.CLEANUP"; /** - * Integer extra indicating the result code of the download. - * TODO: put in link to error list - * TODO: future systemapi (here and and all extras) + * Integer extra indicating the result code of the download. One of + * {@link #RESULT_SUCCESSFUL}, {@link #RESULT_EXPIRED}, or {@link #RESULT_CANCELLED}. */ public static final String EXTRA_RESULT = "android.telephony.mbms.extra.RESULT"; /** * Extra containing the {@link android.telephony.mbms.FileInfo} for which the download result * is for. Must not be null. + * TODO: future systemapi (here and and all extras) except the two for the app intent */ public static final String EXTRA_INFO = "android.telephony.mbms.extra.INFO"; @@ -143,11 +150,23 @@ public class MbmsDownloadManager { public static final String EXTRA_TEMP_FILES_IN_USE = "android.telephony.mbms.extra.TEMP_FILES_IN_USE"; + /** + * Extra containing a single {@link Uri} indicating the location of the successfully + * downloaded file. Set on the intent provided via + * {@link android.telephony.mbms.DownloadRequest.Builder#setAppIntent(Intent)}. + * Will always be set to a non-null value if {@link #EXTRA_RESULT} is set to + * {@link #RESULT_SUCCESSFUL}. + */ + public static final String EXTRA_COMPLETED_FILE_URI = + "android.telephony.mbms.extra.COMPLETED_FILE_URI"; + public static final int RESULT_SUCCESSFUL = 1; public static final int RESULT_CANCELLED = 2; public static final int RESULT_EXPIRED = 3; // TODO - more results! + private static final long BIND_TIMEOUT_MS = 3000; + private final Context mContext; private int mSubId = INVALID_SUBSCRIPTION_ID; @@ -199,12 +218,31 @@ public class MbmsDownloadManager { } private void bindAndInitialize() throws MbmsException { - // TODO: bind - try { - mService.initialize(mDownloadAppName, mSubId, mCallback); - } catch (RemoteException e) { - throw new MbmsException(0); // TODO: proper error code - } + // TODO: fold binding for download and streaming into a common utils class. + final CountDownLatch latch = new CountDownLatch(1); + ServiceConnection bindListener = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mService = IMbmsDownloadService.Stub.asInterface(service); + latch.countDown(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mService = null; + } + }; + + Intent bindIntent = new Intent(); + bindIntent.setComponent(MbmsUtils.toComponentName( + MbmsUtils.getMiddlewareServiceInfo(mContext, MBMS_DOWNLOAD_SERVICE_ACTION))); + + // Kick off the binding, and synchronously wait until binding is complete + mContext.bindService(bindIntent, bindListener, Context.BIND_AUTO_CREATE); + + MbmsUtils.waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS); + + // TODO: initialize } /** @@ -245,6 +283,11 @@ public class MbmsDownloadManager { */ public DownloadRequest download(DownloadRequest request, IDownloadCallback listener) { request.setAppName(mDownloadAppName); + try { + mService.download(request, listener); + } catch (RemoteException e) { + mService = null; + } return request; } diff --git a/telephony/java/android/telephony/MbmsStreamingManager.java b/telephony/java/android/telephony/MbmsStreamingManager.java index e90a63cb7881..f68e2439971f 100644 --- a/telephony/java/android/telephony/MbmsStreamingManager.java +++ b/telephony/java/android/telephony/MbmsStreamingManager.java @@ -27,6 +27,7 @@ import android.os.IBinder; import android.os.RemoteException; import android.telephony.mbms.MbmsException; import android.telephony.mbms.MbmsStreamingManagerCallback; +import android.telephony.mbms.MbmsUtils; import android.telephony.mbms.StreamingService; import android.telephony.mbms.StreamingServiceCallback; import android.telephony.mbms.StreamingServiceInfo; @@ -62,7 +63,9 @@ public class MbmsStreamingManager { Log.i(LOG_TAG, String.format("Connected to service %s", name)); synchronized (MbmsStreamingManager.this) { mService = IMbmsStreamingService.Stub.asInterface(service); - mServiceListeners.forEach(ServiceListener::onServiceConnected); + for (ServiceListener l : mServiceListeners) { + l.onServiceConnected(); + } } } } @@ -72,10 +75,13 @@ public class MbmsStreamingManager { Log.i(LOG_TAG, String.format("Disconnected from service %s", name)); synchronized (MbmsStreamingManager.this) { mService = null; - mServiceListeners.forEach(ServiceListener::onServiceDisconnected); + for (ServiceListener l : mServiceListeners) { + l.onServiceDisconnected(); + } } } }; + private List<ServiceListener> mServiceListeners = new LinkedList<>(); private MbmsStreamingManagerCallback mCallbackToApp; @@ -218,22 +224,6 @@ public class MbmsStreamingManager { } private void bindAndInitialize() throws MbmsException { - // Query for the proper service - PackageManager packageManager = mContext.getPackageManager(); - Intent queryIntent = new Intent(); - queryIntent.setAction(MBMS_STREAMING_SERVICE_ACTION); - List<ResolveInfo> streamingServices = packageManager.queryIntentServices(queryIntent, - PackageManager.MATCH_SYSTEM_ONLY); - - if (streamingServices == null || streamingServices.size() == 0) { - throw new MbmsException( - MbmsException.ERROR_NO_SERVICE_INSTALLED); - } - if (streamingServices.size() > 1) { - throw new MbmsException( - MbmsException.ERROR_MULTIPLE_SERVICES_INSTALLED); - } - // Kick off the binding, and synchronously wait until binding is complete final CountDownLatch latch = new CountDownLatch(1); ServiceListener bindListener = new ServiceListener() { @@ -252,13 +242,14 @@ public class MbmsStreamingManager { } Intent bindIntent = new Intent(); - bindIntent.setComponent(streamingServices.get(0).getComponentInfo().getComponentName()); + bindIntent.setComponent(MbmsUtils.toComponentName( + MbmsUtils.getMiddlewareServiceInfo(mContext, MBMS_STREAMING_SERVICE_ACTION))); mContext.bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE); - waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS); + MbmsUtils.waitOnLatchWithTimeout(latch, BIND_TIMEOUT_MS); - // Remove the listener and call the initialization method through the interface. + // Remove the listener and call the initialization method through the interface. synchronized (this) { mServiceListeners.remove(bindListener); @@ -279,17 +270,4 @@ public class MbmsStreamingManager { } } - private static void waitOnLatchWithTimeout(CountDownLatch l, long timeoutMs) { - long endTime = System.currentTimeMillis() + timeoutMs; - while (System.currentTimeMillis() < endTime) { - try { - l.await(timeoutMs, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - // keep waiting - } - if (l.getCount() <= 0) { - return; - } - } - } } diff --git a/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java new file mode 100644 index 000000000000..c01ddaedbd88 --- /dev/null +++ b/telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java @@ -0,0 +1,365 @@ +/* + * 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.mbms; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.telephony.MbmsDownloadManager; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +/** + * @hide + */ +public class MbmsDownloadReceiver extends BroadcastReceiver { + private static final String LOG_TAG = "MbmsDownloadReceiver"; + private static final String TEMP_FILE_SUFFIX = ".embms.temp"; + private static final int MAX_TEMP_FILE_RETRIES = 5; + + public static final String MBMS_FILE_PROVIDER_META_DATA_KEY = "mbms-file-provider-authority"; + + private String mFileProviderAuthorityCache = null; + private String mMiddlewarePackageNameCache = null; + + @Override + public void onReceive(Context context, Intent intent) { + if (!verifyIntentContents(intent)) { + setResultCode(1 /* TODO: define error constants */); + return; + } + + if (MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { + moveDownloadedFile(context, intent); + cleanupPostMove(context, intent); + } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { + generateTempFiles(context, intent); + } + // TODO: Add handling for ACTION_CLEANUP + } + + private boolean verifyIntentContents(Intent intent) { + if (MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL.equals(intent.getAction())) { + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_RESULT)) { + Log.w(LOG_TAG, "Download result did not include a result code. Ignoring."); + return false; + } + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_REQUEST)) { + Log.w(LOG_TAG, "Download result did not include the associated request. Ignoring."); + return false; + } + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_INFO)) { + Log.w(LOG_TAG, "Download result did not include the associated file info. " + + "Ignoring."); + return false; + } + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_FINAL_URI)) { + Log.w(LOG_TAG, "Download result did not include the path to the final " + + "temp file. Ignoring."); + return false; + } + return true; + } else if (MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST.equals(intent.getAction())) { + if (!intent.hasExtra(MbmsDownloadManager.EXTRA_REQUEST)) { + Log.w(LOG_TAG, "Temp file request not include the associated request. Ignoring."); + return false; + } + return true; + } + + Log.w(LOG_TAG, "Received intent with unknown action: " + intent.getAction()); + return false; + } + + private void moveDownloadedFile(Context context, Intent intent) { + DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); + // TODO: check request against token + Intent intentForApp = request.getIntentForApp(); + + int result = intent.getIntExtra(MbmsDownloadManager.EXTRA_RESULT, + MbmsDownloadManager.RESULT_CANCELLED); + intentForApp.putExtra(MbmsDownloadManager.EXTRA_RESULT, result); + + if (result != MbmsDownloadManager.RESULT_SUCCESSFUL) { + Log.i(LOG_TAG, "Download request indicated a failed download. Aborting."); + context.sendBroadcast(intentForApp); + return; + } + + Uri destinationUri = request.getDestinationUri(); + Uri finalTempFile = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_FINAL_URI); + if (!verifyTempFilePath(context, request, finalTempFile)) { + Log.w(LOG_TAG, "Download result specified an invalid temp file " + finalTempFile); + setResultCode(1); + return; + } + + String relativePath = calculateDestinationFileRelativePath(request, + (FileInfo) intent.getParcelableExtra(MbmsDownloadManager.EXTRA_INFO)); + + if (!moveTempFile(finalTempFile, destinationUri, relativePath)) { + Log.w(LOG_TAG, "Failed to move temp file to final destination"); + setResultCode(1); + } + + context.sendBroadcast(intentForApp); + setResultCode(0); + } + + private void cleanupPostMove(Context context, Intent intent) { + // TODO: account for in-use temp files + DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); + if (request == null) { + Log.w(LOG_TAG, "Intent does not include a DownloadRequest. Ignoring."); + return; + } + + List<Uri> tempFiles = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_TEMP_LIST); + if (tempFiles == null) { + return; + } + + for (Uri tempFileUri : tempFiles) { + if (verifyTempFilePath(context, request, tempFileUri)) { + File tempFile = new File(tempFileUri.getSchemeSpecificPart()); + tempFile.delete(); + } + } + } + + private void generateTempFiles(Context context, Intent intent) { + // TODO: update pursuant to final decision on temp file locations + DownloadRequest request = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_REQUEST); + if (request == null) { + Log.w(LOG_TAG, "Temp file request did not include the associated request. Ignoring."); + setResultCode(1 /* TODO: define error constants */); + return; + } + int fdCount = intent.getIntExtra(MbmsDownloadManager.EXTRA_FD_COUNT, 0); + List<Uri> pausedList = intent.getParcelableExtra(MbmsDownloadManager.EXTRA_PAUSED_LIST); + + if (fdCount == 0 && (pausedList == null || pausedList.size() == 0)) { + Log.i(LOG_TAG, "No temp files actually requested. Ending."); + setResultCode(0); + setResultExtras(Bundle.EMPTY); + return; + } + + ArrayList<UriPathPair> freshTempFiles = generateFreshTempFiles(context, request, fdCount); + ArrayList<UriPathPair> pausedFiles = + generateUrisForPausedFiles(context, request, pausedList); + + Bundle result = new Bundle(); + result.putParcelableArrayList(MbmsDownloadManager.EXTRA_FREE_URI_LIST, freshTempFiles); + result.putParcelableArrayList(MbmsDownloadManager.EXTRA_PAUSED_URI_LIST, pausedFiles); + setResultExtras(result); + } + + private ArrayList<UriPathPair> generateFreshTempFiles(Context context, DownloadRequest request, + int freshFdCount) { + File tempFileDir = getEmbmsTempFileDirForRequest(context, request); + if (!tempFileDir.exists()) { + tempFileDir.mkdirs(); + } + + // Name the files with the template "N-UUID", where N is the request ID and UUID is a + // random uuid. + ArrayList<UriPathPair> result = new ArrayList<>(freshFdCount); + for (int i = 0; i < freshFdCount; i++) { + File tempFile = generateSingleTempFile(tempFileDir); + if (tempFile == null) { + setResultCode(2 /* TODO: define error constants */); + Log.w(LOG_TAG, "Failed to generate a temp file. Moving on."); + continue; + } + Uri fileUri = Uri.fromParts(ContentResolver.SCHEME_FILE, tempFile.getPath(), null); + Uri contentUri = MbmsTempFileProvider.getUriForFile( + context, getFileProviderAuthorityCached(context), tempFile); + context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + result.add(new UriPathPair(fileUri, contentUri)); + } + + return result; + } + + private static File generateSingleTempFile(File tempFileDir) { + int numTries = 0; + while (numTries < MAX_TEMP_FILE_RETRIES) { + numTries++; + String fileName = UUID.randomUUID() + TEMP_FILE_SUFFIX; + File tempFile = new File(tempFileDir, fileName); + try { + if (tempFile.createNewFile()) { + return tempFile.getCanonicalFile(); + } + } catch (IOException e) { + continue; + } + } + return null; + } + + + private ArrayList<UriPathPair> generateUrisForPausedFiles(Context context, + DownloadRequest request, List<Uri> pausedFiles) { + if (pausedFiles == null) { + return new ArrayList<>(0); + } + ArrayList<UriPathPair> result = new ArrayList<>(pausedFiles.size()); + + for (Uri fileUri : pausedFiles) { + if (!verifyTempFilePath(context, request, fileUri)) { + Log.w(LOG_TAG, "Supplied file " + fileUri + " is not a valid temp file to resume"); + setResultCode(2 /* TODO: define error codes */); + continue; + } + File tempFile = new File(fileUri.getSchemeSpecificPart()); + if (!tempFile.exists()) { + Log.w(LOG_TAG, "Supplied file " + fileUri + " does not exist."); + setResultCode(2 /* TODO: define error codes */); + continue; + } + Uri contentUri = MbmsTempFileProvider.getUriForFile( + context, getFileProviderAuthorityCached(context), tempFile); + context.grantUriPermission(getMiddlewarePackageCached(context), contentUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + result.add(new UriPathPair(fileUri, contentUri)); + } + return result; + } + + private static String calculateDestinationFileRelativePath(DownloadRequest request, + FileInfo info) { + // TODO: determine whether this is actually the path determination scheme we want to use + List<String> filePathComponents = info.uri.getPathSegments(); + List<String> requestPathComponents = request.getSourceUri().getPathSegments(); + Iterator<String> filePathIter = filePathComponents.iterator(); + Iterator<String> requestPathIter = requestPathComponents.iterator(); + + LinkedList<String> relativePathComponents = new LinkedList<>(); + while (filePathIter.hasNext()) { + String currFilePathComponent = filePathIter.next(); + if (requestPathIter.hasNext()) { + String requestFilePathComponent = requestPathIter.next(); + if (requestFilePathComponent.equals(currFilePathComponent)) { + continue; + } + } + relativePathComponents.add(currFilePathComponent); + } + return String.join("/", relativePathComponents); + } + + private static boolean moveTempFile(Uri fromPath, Uri toPath, String relativePath) { + if (!ContentResolver.SCHEME_FILE.equals(fromPath.getScheme())) { + Log.w(LOG_TAG, "Moving source uri " + fromPath+ " does not have a file scheme"); + return false; + } + if (!ContentResolver.SCHEME_FILE.equals(toPath.getScheme())) { + Log.w(LOG_TAG, "Moving destination uri " + toPath + " does not have a file scheme"); + return false; + } + + File fromFile = new File(fromPath.getSchemeSpecificPart()); + File toFile = new File(toPath.getSchemeSpecificPart(), relativePath); + toFile.getParentFile().mkdirs(); + + // TODO: This may not work if the two files are on different filesystems. Should we + // enforce that the temp file storage and the permanent storage are both in the same fs? + return fromFile.renameTo(toFile); + } + + private static boolean verifyTempFilePath(Context context, DownloadRequest request, + Uri filePath) { + // TODO: modify pursuant to final decision on temp file path scheme + if (!ContentResolver.SCHEME_FILE.equals(filePath.getScheme())) { + Log.w(LOG_TAG, "Uri " + filePath + " does not have a file scheme"); + return false; + } + + String path = filePath.getSchemeSpecificPart(); + File tempFile = new File(path); + if (!tempFile.exists()) { + Log.w(LOG_TAG, "File at " + path + " does not exist."); + return false; + } + + if (!MbmsUtils.isContainedIn(getEmbmsTempFileDirForRequest(context, request), tempFile)) { + return false; + } + + return true; + } + + /** + * Returns a File linked to the directory used to store temp files for this request + */ + private static File getEmbmsTempFileDirForRequest(Context context, DownloadRequest request) { + File embmsTempFileDir = MbmsTempFileProvider.getEmbmsTempFileDir( + context, getFileProviderAuthority(context)); + + // TODO: better naming scheme for temp file dirs + String tempFileDirName = String.valueOf(request.getFileServiceInfo().getServiceId()); + return new File(embmsTempFileDir, tempFileDirName); + } + + private String getFileProviderAuthorityCached(Context context) { + if (mFileProviderAuthorityCache != null) { + return mFileProviderAuthorityCache; + } + + mFileProviderAuthorityCache = getFileProviderAuthority(context); + return mFileProviderAuthorityCache; + } + + private static String getFileProviderAuthority(Context context) { + ApplicationInfo appInfo; + try { + appInfo = context.getPackageManager() + .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException("Package manager couldn't find " + context.getPackageName()); + } + String authority = appInfo.metaData.getString(MBMS_FILE_PROVIDER_META_DATA_KEY); + if (authority == null) { + throw new RuntimeException("Must declare the file provider authority as meta data"); + } + return authority; + } + + private String getMiddlewarePackageCached(Context context) { + if (mMiddlewarePackageNameCache == null) { + mMiddlewarePackageNameCache = MbmsUtils.getMiddlewareServiceInfo(context, + MbmsDownloadManager.MBMS_DOWNLOAD_SERVICE_ACTION).packageName; + } + return mMiddlewarePackageNameCache; + } +} diff --git a/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java b/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java new file mode 100644 index 000000000000..9842581cdc02 --- /dev/null +++ b/telephony/java/android/telephony/mbms/MbmsTempFileProvider.java @@ -0,0 +1,193 @@ +/* + * 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.mbms; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * @hide + */ +public class MbmsTempFileProvider extends ContentProvider { + public static final String META_DATA_USE_EXTERNAL_STORAGE = "use-external-storage"; + public static final String META_DATA_TEMP_FILE_DIRECTORY = "temp-file-path"; + public static final String DEFAULT_TOP_LEVEL_TEMP_DIRECTORY = "androidMbmsTempFileRoot"; + + private String mAuthority; + private Context mContext; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, + @Nullable String selection, @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + throw new UnsupportedOperationException("No querying supported"); + } + + @Override + public String getType(@NonNull Uri uri) { + // EMBMS temp files can contain arbitrary content. + return "application/octet-stream"; + } + + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + throw new UnsupportedOperationException("No inserting supported"); + } + + @Override + public int delete(@NonNull Uri uri, @Nullable String selection, + @Nullable String[] selectionArgs) { + throw new UnsupportedOperationException("No deleting supported"); + } + + @Override + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String + selection, @Nullable String[] selectionArgs) { + throw new UnsupportedOperationException("No updating supported"); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + // ContentProvider has already checked granted permissions + final File file = getFileForUri(mContext, mAuthority, uri); + final int fileMode = ParcelFileDescriptor.parseMode(mode); + return ParcelFileDescriptor.open(file, fileMode); + } + + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + + // Sanity check our security + if (info.exported) { + throw new SecurityException("Provider must not be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grant uri permissions"); + } + + mAuthority = info.authority; + mContext = context; + } + + public static Uri getUriForFile(Context context, String authority, File file) { + // Get the canonical path of the temp file + String filePath; + try { + filePath = file.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException("Could not get canonical path for file " + file); + } + + // Make sure the temp file is contained in the temp file directory as configured in the + // manifest + File tempFileDir = getEmbmsTempFileDir(context, authority); + if (!MbmsUtils.isContainedIn(tempFileDir, file)) { + throw new IllegalArgumentException("File " + file + " is not contained in the temp " + + "file directory, which is " + tempFileDir); + } + + // Get the canonical path of the temp file directory + String tempFileDirPath; + try { + tempFileDirPath = tempFileDir.getCanonicalPath(); + } catch (IOException e) { + throw new RuntimeException( + "Could not get canonical path for temp file root dir " + tempFileDir); + } + + // Start at first char of path under temp file directory + String pathFragment; + if (tempFileDirPath.endsWith("/")) { + pathFragment = filePath.substring(tempFileDirPath.length()); + } else { + pathFragment = filePath.substring(tempFileDirPath.length() + 1); + } + + String encodedPath = Uri.encode(pathFragment); + return new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority).encodedPath(encodedPath).build(); + } + + public static File getFileForUri(Context context, String authority, Uri uri) + throws FileNotFoundException { + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + throw new IllegalArgumentException("Uri must have scheme content"); + } + + String relPath = Uri.decode(uri.getEncodedPath()); + File file; + File tempFileDir; + + try { + tempFileDir = getEmbmsTempFileDir(context, authority).getCanonicalFile(); + file = new File(tempFileDir, relPath).getCanonicalFile(); + } catch (IOException e) { + throw new FileNotFoundException("Could not resolve paths"); + } + + if (!file.getPath().startsWith(tempFileDir.getPath())) { + throw new SecurityException("Resolved path jumped beyond configured root"); + } + + return file; + } + + /** + * Returns a File for the directory used to store temp files for this app + */ + public static File getEmbmsTempFileDir(Context context, String authority) { + Bundle metadata = getMetadata(context, authority); + File parentDirectory; + if (metadata.getBoolean(META_DATA_USE_EXTERNAL_STORAGE, false)) { + parentDirectory = context.getExternalFilesDir(null); + } else { + parentDirectory = context.getFilesDir(); + } + + String tmpFilePath = metadata.getString(META_DATA_TEMP_FILE_DIRECTORY); + if (tmpFilePath == null) { + tmpFilePath = DEFAULT_TOP_LEVEL_TEMP_DIRECTORY; + } + return new File(parentDirectory, tmpFilePath); + } + + private static Bundle getMetadata(Context context, String authority) { + final ProviderInfo info = context.getPackageManager() + .resolveContentProvider(authority, PackageManager.GET_META_DATA); + return info.metaData; + } +} diff --git a/telephony/java/android/telephony/mbms/MbmsUtils.java b/telephony/java/android/telephony/mbms/MbmsUtils.java new file mode 100644 index 000000000000..de308053df56 --- /dev/null +++ b/telephony/java/android/telephony/mbms/MbmsUtils.java @@ -0,0 +1,86 @@ +/* + * 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.mbms; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.*; +import android.content.pm.ServiceInfo; +import android.telephony.MbmsDownloadManager; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * @hide + */ +public class MbmsUtils { + private static final String LOG_TAG = "MbmsUtils"; + + public static boolean isContainedIn(File parent, File child) { + try { + String parentPath = parent.getCanonicalPath(); + String childPath = child.getCanonicalPath(); + return childPath.startsWith(parentPath); + } catch (IOException e) { + throw new RuntimeException("Failed to resolve canonical paths: " + e); + } + } + + public static void waitOnLatchWithTimeout(CountDownLatch l, long timeoutMs) { + long endTime = System.currentTimeMillis() + timeoutMs; + while (System.currentTimeMillis() < endTime) { + try { + l.await(timeoutMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // keep waiting + } + if (l.getCount() <= 0) { + return; + } + } + } + + public static ComponentName toComponentName(ComponentInfo ci) { + return new ComponentName(ci.packageName, ci.name); + } + + public static ServiceInfo getMiddlewareServiceInfo(Context context, String serviceAction) { + // Query for the proper service + PackageManager packageManager = context.getPackageManager(); + Intent queryIntent = new Intent(); + queryIntent.setAction(serviceAction); + List<ResolveInfo> downloadServices = packageManager.queryIntentServices(queryIntent, + PackageManager.MATCH_SYSTEM_ONLY); + + if (downloadServices == null || downloadServices.size() == 0) { + Log.w(LOG_TAG, "No download services found, cannot get service info"); + return null; + } + + if (downloadServices.size() > 1) { + Log.w(LOG_TAG, "More than one download service found, cannot get unique service"); + return null; + } + return downloadServices.get(0).serviceInfo; + } +} |