OSDN Git Service

Embms download setup
authorHall Liu <hallliu@google.com>
Fri, 19 May 2017 22:28:09 +0000 (15:28 -0700)
committerHall Liu <hallliu@google.com>
Wed, 7 Jun 2017 20:58:18 +0000 (13:58 -0700)
Add MbmsDownloadReceiver and MbmsTempFileProvider to facilitate MBMS
download.
Fill in the download method in MbmsDownloadManager

Test: testapps
Change-Id: Ibb30f3b2b9d14b9df6b53c06916c21e3ca3a42c6

telephony/java/android/telephony/MbmsDownloadManager.java
telephony/java/android/telephony/MbmsStreamingManager.java
telephony/java/android/telephony/mbms/MbmsDownloadReceiver.java [new file with mode: 0644]
telephony/java/android/telephony/mbms/MbmsTempFileProvider.java [new file with mode: 0644]
telephony/java/android/telephony/mbms/MbmsUtils.java [new file with mode: 0644]

index a9ec299..c8c6d01 100644 (file)
 
 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;
     }
 
index e90a63c..f68e243 100644 (file)
@@ -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 (file)
index 0000000..c01ddae
--- /dev/null
@@ -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 (file)
index 0000000..9842581
--- /dev/null
@@ -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 (file)
index 0000000..de30805
--- /dev/null
@@ -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;
+    }
+}