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;
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
"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";
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;
}
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
}
/**
*/
public DownloadRequest download(DownloadRequest request, IDownloadCallback listener) {
request.setAppName(mDownloadAppName);
+ try {
+ mService.download(request, listener);
+ } catch (RemoteException e) {
+ mService = null;
+ }
return request;
}
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;
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();
+ }
}
}
}
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;
}
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() {
}
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);
}
}
- 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;
- }
- }
- }
}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}