Embms download part 2 am: eab21d93e8
am: a74425d9c6

Change-Id: I78ec6f7dbd1b32d452b8a1558359eb308588687c
diff --git a/testapps/EmbmsServiceTestApp/Android.mk b/testapps/EmbmsServiceTestApp/Android.mk
index 874fad2..d8c4493 100644
--- a/testapps/EmbmsServiceTestApp/Android.mk
+++ b/testapps/EmbmsServiceTestApp/Android.mk
@@ -13,8 +13,7 @@
 
 LOCAL_CERTIFICATE := platform
 LOCAL_PRIVILEGED_MODULE := true
-# Change the following to "debug" to build the EmbmsTestService into the userdebug build.
-LOCAL_MODULE_TAGS := optional
-#LOCAL_MODULE_TAGS := debug
+# Uncomment the following line to build the EmbmsTestService into the userdebug build.
+# LOCAL_MODULE_TAGS := debug
 
 include $(BUILD_PACKAGE)
diff --git a/testapps/EmbmsServiceTestApp/res/raw/sheep.png b/testapps/EmbmsServiceTestApp/res/raw/sheep.png
new file mode 100644
index 0000000..650966c
--- /dev/null
+++ b/testapps/EmbmsServiceTestApp/res/raw/sheep.png
Binary files differ
diff --git a/testapps/EmbmsServiceTestApp/res/raw/snake.png b/testapps/EmbmsServiceTestApp/res/raw/snake.png
new file mode 100644
index 0000000..6fa6c8b
--- /dev/null
+++ b/testapps/EmbmsServiceTestApp/res/raw/snake.png
Binary files differ
diff --git a/testapps/EmbmsServiceTestApp/res/raw/s1.png b/testapps/EmbmsServiceTestApp/res/raw/suntree.png
similarity index 100%
rename from testapps/EmbmsServiceTestApp/res/raw/s1.png
rename to testapps/EmbmsServiceTestApp/res/raw/suntree.png
Binary files differ
diff --git a/testapps/EmbmsServiceTestApp/res/raw/unicorn.png b/testapps/EmbmsServiceTestApp/res/raw/unicorn.png
new file mode 100644
index 0000000..3146344
--- /dev/null
+++ b/testapps/EmbmsServiceTestApp/res/raw/unicorn.png
Binary files differ
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/AppActiveStreams.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/AppActiveStreams.java
index 6a0f61e..9b3cc6b 100644
--- a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/AppActiveStreams.java
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/AppActiveStreams.java
@@ -67,10 +67,10 @@
 
     // Stores the state and callback per service ID.
     private final Map<String, StreamCallbackWithState> mStreamStates = new HashMap<>();
-    private final StreamingAppIdentifier mAppIdentifier;
+    private final FrontendAppIdentifier mAppIdentifier;
     private final Random mRand = new Random();
 
-    public AppActiveStreams(StreamingAppIdentifier appIdentifier) {
+    public AppActiveStreams(FrontendAppIdentifier appIdentifier) {
         mAppIdentifier = appIdentifier;
     }
 
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsSampleDownloadService.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsSampleDownloadService.java
index 360dd15..490f3fa 100644
--- a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsSampleDownloadService.java
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsSampleDownloadService.java
@@ -29,10 +29,13 @@
 import android.os.HandlerThread;
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
 import android.telephony.MbmsDownloadManager;
 import android.telephony.mbms.DownloadRequest;
+import android.telephony.mbms.FileInfo;
+import android.telephony.mbms.FileServiceInfo;
 import android.telephony.mbms.IDownloadCallback;
-import android.telephony.mbms.MbmsDownloadReceiver;
+import android.telephony.mbms.IMbmsDownloadManagerCallback;
 import android.telephony.mbms.MbmsException;
 import android.telephony.mbms.UriPathPair;
 import android.telephony.mbms.vendor.IMbmsDownloadService;
@@ -44,33 +47,111 @@
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 public class EmbmsSampleDownloadService extends Service {
+    private static final Set<String> ALLOWED_PACKAGES = new HashSet<String>() {{
+        add("com.android.phone.testapps.embmsdownload");
+    }};
+
     private static final String LOG_TAG = "EmbmsSampleDownload";
+    private static final long SEND_FILE_SERVICE_INFO_DELAY = 500;
     private static final long DOWNLOAD_DELAY_MS = 1000;
+    private static final long FILE_SEPARATION_DELAY = 500;
 
     private final IMbmsDownloadService mBinder = new MbmsDownloadServiceBase() {
         @Override
-        public int download(DownloadRequest downloadRequest, IDownloadCallback listener) {
-            // TODO: move this package name finding logic to initialize()
+        public void initialize(String appName, int subId, IMbmsDownloadManagerCallback listener) {
             String[] packageNames = getPackageManager().getPackagesForUid(Binder.getCallingUid());
             if (packageNames == null) {
                 throw new SecurityException("No matching packages found for your UID");
             }
-
-            if (packageNames.length != 1) {
-                throw new IllegalStateException("More than one package found for your UID");
+            boolean isUidAllowed = Arrays.stream(packageNames).anyMatch(ALLOWED_PACKAGES::contains);
+            if (!isUidAllowed) {
+                throw new SecurityException("No packages for your UID are allowed to use this " +
+                        "service");
             }
 
-            String packageName = packageNames[0];
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subId);
+            if (!mAppCallbacks.containsKey(appKey)) {
+                mAppCallbacks.put(appKey, listener);
+                ComponentName appReceiver = MbmsDownloadManager.getAppReceiverFromUid(
+                        EmbmsSampleDownloadService.this, Binder.getCallingUid());
+                mAppReceivers.put(appKey, appReceiver);
+            } else {
+                // Stick the error callback on a different thread so that we're not calling back
+                // to the app on the same thread.
+                mHandler.post(() -> {
+                    try {
+                        listener.error(MbmsException.ERROR_ALREADY_INITIALIZED, "");
+                    } catch (RemoteException e) {
+                        // ignore, it was an error anyway
+                    }
+                });
+            }
+        }
 
-            mHandler.post(() -> sendFdRequest(downloadRequest, packageName, 1));
+        @Override
+        public int getFileServices(String appName, int subscriptionId,
+                List<String> serviceClasses) throws RemoteException {
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            checkInitialized(appKey);
+
+            List<FileServiceInfo> serviceInfos =
+                    FileServiceRepository.getInstance(EmbmsSampleDownloadService.this)
+                    .getFileServicesForClasses(serviceClasses);
+
+            mHandler.postDelayed(() -> {
+                try {
+                    IMbmsDownloadManagerCallback appCallback = mAppCallbacks.get(appKey);
+                    appCallback.fileServicesUpdated(serviceInfos);
+                } catch (RemoteException e) {
+                    // TODO: call dispose
+                }
+            }, SEND_FILE_SERVICE_INFO_DELAY);
+            return MbmsException.SUCCESS;
+        }
+
+        @Override
+        public int setTempFileRootDirectory(String appName, int subscriptionId,
+                String rootDirectoryPath) throws RemoteException {
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            checkInitialized(appKey);
+
+            if (mDoesAppHaveActiveDownload.getOrDefault(appKey, false)) {
+                return MbmsException.ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT;
+            }
+            mAppTempFileRoots.put(appKey, rootDirectoryPath);
+            return MbmsException.SUCCESS;
+        }
+
+        @Override
+        public int download(DownloadRequest downloadRequest, IDownloadCallback listener) {
+            FrontendAppIdentifier appKey = new FrontendAppIdentifier(
+                    Binder.getCallingUid(), downloadRequest.getAppName(),
+                    downloadRequest.getSubscriptionId());
+            checkInitialized(appKey);
+
+            mHandler.post(() -> sendFdRequest(downloadRequest, appKey));
             return MbmsException.SUCCESS;
         }
     };
 
+    private final Map<FrontendAppIdentifier, IMbmsDownloadManagerCallback> mAppCallbacks =
+            new HashMap<>();
+    private final Map<FrontendAppIdentifier, ComponentName> mAppReceivers = new HashMap<>();
+    private final Map<FrontendAppIdentifier, String> mAppTempFileRoots = new HashMap<>();
+    private final Map<FrontendAppIdentifier, Boolean> mDoesAppHaveActiveDownload =
+            new ConcurrentHashMap<>();
+
     private HandlerThread mHandlerThread;
     private Handler mHandler;
 
@@ -82,14 +163,15 @@
         return mBinder.asBinder();
     }
 
-    private void sendFdRequest(DownloadRequest request, String packageName, int numFds) {
+    private void sendFdRequest(DownloadRequest request, FrontendAppIdentifier appKey) {
+        int numFds = getNumFdsNeededForRequest(request);
         // Compose the FILE_DESCRIPTOR_REQUEST_INTENT
         Intent requestIntent = new Intent(MbmsDownloadManager.ACTION_FILE_DESCRIPTOR_REQUEST);
         requestIntent.putExtra(MbmsDownloadManager.EXTRA_REQUEST, request);
         requestIntent.putExtra(MbmsDownloadManager.EXTRA_FD_COUNT, numFds);
-        ComponentName mbmsReceiverComponent = new ComponentName(packageName,
-                MbmsDownloadReceiver.class.getCanonicalName());
-        requestIntent.setComponent(mbmsReceiverComponent);
+        requestIntent.putExtra(MbmsDownloadManager.EXTRA_TEMP_FILE_ROOT,
+                mAppTempFileRoots.get(appKey));
+        requestIntent.setComponent(mAppReceivers.get(appKey));
 
         // Send as an ordered broadcast, using a BroadcastReceiver to capture the result
         // containing UriPathPairs.
@@ -102,7 +184,7 @@
                         // This delay is to emulate the time it'd usually take to fetch the file
                         // off the network.
                         mHandler.postDelayed(
-                                () -> performDownload(request, packageName, resultExtras),
+                                () -> performDownload(request, appKey, resultExtras),
                                 DOWNLOAD_DELAY_MS);
                     }
                 },
@@ -112,25 +194,52 @@
                 null /* initialExtras */);
     }
 
-    private void performDownload(DownloadRequest request, String packageName, Bundle extras) {
-        int result = MbmsDownloadManager.RESULT_SUCCESSFUL;
+    private void performDownload(DownloadRequest request, FrontendAppIdentifier appKey,
+            Bundle extras) {
         List<UriPathPair> tempFiles = extras.getParcelableArrayList(
                 MbmsDownloadManager.EXTRA_FREE_URI_LIST);
-        Uri tempFilePathUri = tempFiles.get(0).getFilePathUri();
-        Uri freeTempFileUri = tempFiles.get(0).getContentUri();
+        List<FileInfo> filesToDownload = request.getFileServiceInfo().getFiles();
 
+        if (tempFiles.size() != filesToDownload.size()) {
+            Log.w(LOG_TAG, "Different numbers of temp files and files to download...");
+        }
+
+        // Go through the files one-by-one and send them to the frontend app with a delay between
+        // each one.
+        mDoesAppHaveActiveDownload.put(appKey, true);
+        for (int i = 0; i < tempFiles.size(); i++) {
+            if (i >= filesToDownload.size()) {
+                break;
+            }
+            UriPathPair tempFile = tempFiles.get(i);
+            FileInfo fileToDownload = filesToDownload.get(i);
+            final boolean isLastFile = i == tempFiles.size() - 1;
+            mHandler.postDelayed(() -> {
+                downloadSingleFile(appKey, request, tempFile, fileToDownload);
+                if (isLastFile) {
+                    mDoesAppHaveActiveDownload.put(appKey, false);
+                }
+            }, FILE_SEPARATION_DELAY * i);
+        }
+    }
+
+    private void downloadSingleFile(FrontendAppIdentifier appKey, DownloadRequest request,
+            UriPathPair tempFile, FileInfo fileToDownload) {
+        int result = MbmsDownloadManager.RESULT_SUCCESSFUL;
         try {
             // Get the ParcelFileDescriptor for the single temp file we requested
-            ParcelFileDescriptor tempFile = getContentResolver().openFileDescriptor(
-                    freeTempFileUri, "rw");
+            ParcelFileDescriptor tempFileFd = getContentResolver().openFileDescriptor(
+                    tempFile.getContentUri(), "rw");
             OutputStream destinationStream =
-                    new ParcelFileDescriptor.AutoCloseOutputStream(tempFile);
+                    new ParcelFileDescriptor.AutoCloseOutputStream(tempFileFd);
 
             // This is how you get the native fd
-            Log.i(LOG_TAG, "Native fd: " + tempFile.getFd());
+            Log.i(LOG_TAG, "Native fd: " + tempFileFd.getFd());
 
+            int resourceId = FileServiceRepository.getInstance(this)
+                    .getResourceForFileUri(fileToDownload.getUri());
             // Open the picture we have in our res/raw directory
-            InputStream image = getResources().openRawResource(R.raw.s1);
+            InputStream image = getResources().openRawResource(resourceId);
 
             // Copy it into the temp file in the app's file space (crudely)
             byte[] imageBuffer = new byte[image.available()];
@@ -144,16 +253,17 @@
         Intent downloadResultIntent =
                 new Intent(MbmsDownloadManager.ACTION_DOWNLOAD_RESULT_INTERNAL);
         downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_REQUEST, request);
-        downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_FINAL_URI, tempFilePathUri);
+        downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_FINAL_URI,
+                tempFile.getFilePathUri());
+        downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_FILE_INFO, fileToDownload);
+        downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_TEMP_FILE_ROOT,
+                mAppTempFileRoots.get(appKey));
         ArrayList<Uri> tempFileList = new ArrayList<>(1);
-        tempFileList.add(tempFilePathUri);
+        tempFileList.add(tempFile.getFilePathUri());
         downloadResultIntent.getExtras().putParcelableArrayList(
                 MbmsDownloadManager.EXTRA_TEMP_LIST, tempFileList);
         downloadResultIntent.putExtra(MbmsDownloadManager.EXTRA_RESULT, result);
-
-        ComponentName mbmsReceiverComponent = new ComponentName(packageName,
-                MbmsDownloadReceiver.class.getCanonicalName());
-        downloadResultIntent.setComponent(mbmsReceiverComponent);
+        downloadResultIntent.setComponent(mAppReceivers.get(appKey));
 
         sendOrderedBroadcast(downloadResultIntent,
                 null, // receiverPermission
@@ -169,4 +279,14 @@
                 null, // initialData
                 null /* initialExtras */);
     }
+
+    private void checkInitialized(FrontendAppIdentifier appKey) {
+        if (!mAppCallbacks.containsKey(appKey)) {
+            throw new IllegalStateException("Not yet initialized");
+        }
+    }
+
+    private int getNumFdsNeededForRequest(DownloadRequest request) {
+        return request.getFileServiceInfo().getFiles().size();
+    }
 }
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsTestStreamingService.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsTestStreamingService.java
index 9758d49..11a9bde 100644
--- a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsTestStreamingService.java
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/EmbmsTestStreamingService.java
@@ -54,7 +54,7 @@
 
     private static final int SEND_STREAMING_SERVICES_LIST = 1;
 
-    private final Map<StreamingAppIdentifier, IMbmsStreamingManagerCallback> mAppCallbacks =
+    private final Map<FrontendAppIdentifier, IMbmsStreamingManagerCallback> mAppCallbacks =
             new HashMap<>();
 
     private HandlerThread mHandlerThread;
@@ -63,7 +63,7 @@
         switch (msg.what) {
             case SEND_STREAMING_SERVICES_LIST:
                 SomeArgs args = (SomeArgs) msg.obj;
-                StreamingAppIdentifier appKey = (StreamingAppIdentifier) args.arg1;
+                FrontendAppIdentifier appKey = (FrontendAppIdentifier) args.arg1;
                 List<StreamingServiceInfo> services = (List) args.arg2;
                 IMbmsStreamingManagerCallback appCallback = mAppCallbacks.get(appKey);
                 if (appCallback != null) {
@@ -91,8 +91,8 @@
                         "service");
             }
 
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subId);
             if (!mAppCallbacks.containsKey(appKey)) {
                 mAppCallbacks.put(appKey, listener);
             } else {
@@ -104,8 +104,8 @@
         @Override
         public int getStreamingServices(String appName, int subscriptionId,
                 List<String> serviceClasses) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
 
             List<StreamingServiceInfo> serviceInfos =
@@ -125,8 +125,8 @@
         @Override
         public int startStreaming(String appName, int subscriptionId, String serviceId,
                 IStreamingServiceCallback callback) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
             checkServiceExists(serviceId);
 
@@ -143,8 +143,8 @@
 
         @Override
         public Uri getPlaybackUri(String appName, int subscriptionId, String serviceId) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
             checkServiceExists(serviceId);
 
@@ -157,8 +157,8 @@
 
         @Override
         public void stopStreaming(String appName, int subscriptionId, String serviceId) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
             checkServiceExists(serviceId);
 
@@ -167,8 +167,8 @@
 
         @Override
         public void disposeStream(String appName, int subscriptionId, String serviceId) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
             checkServiceExists(serviceId);
 
@@ -178,8 +178,8 @@
 
         @Override
         public void dispose(String appName, int subscriptionId) {
-            StreamingAppIdentifier appKey =
-                    new StreamingAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
+            FrontendAppIdentifier appKey =
+                    new FrontendAppIdentifier(Binder.getCallingUid(), appName, subscriptionId);
             checkInitialized(appKey);
 
             Log.i(TAG, "Disposing app " + appName);
@@ -208,7 +208,7 @@
         Log.d(TAG, s);
     }
 
-    private void checkInitialized(StreamingAppIdentifier appKey) {
+    private void checkInitialized(FrontendAppIdentifier appKey) {
         if (!mAppCallbacks.containsKey(appKey)) {
             throw new IllegalStateException("Not yet initialized");
         }
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FileServiceRepository.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FileServiceRepository.java
new file mode 100644
index 0000000..4d0b6c3
--- /dev/null
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FileServiceRepository.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.phone.testapps.embmsmw;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.mbms.FileInfo;
+import android.telephony.mbms.FileServiceInfo;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class FileServiceRepository {
+    private int sServiceIdCounter = 0;
+    private final Map<String, FileServiceInfo> mIdToServiceInfo = new HashMap<>();
+    private final Map<Uri, Integer> mFileUriToResource = new HashMap<>();
+
+    private static final String FILE_DOWNLOAD_SCHEME = "filedownload";
+    private static final String FILE_AUTHORITY = "com.android.phone.testapps";
+
+    private static FileServiceRepository sInstance;
+    public static FileServiceRepository getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new FileServiceRepository(context);
+        }
+        return sInstance;
+    }
+
+    private final Context mContext;
+
+    private FileServiceRepository(Context context) {
+        mContext = context;
+        Uri sunAndTree = initFile("sunAndTree.png", R.raw.suntree);
+        Uri snake = initFile("animals/snake.png", R.raw.snake);
+        Uri unicorn = initFile("animals/unicorn.png", R.raw.unicorn);
+        Uri sheep = initFile("animals/sheep.png", R.raw.sheep);
+
+        createFileService("Class1", sunAndTree);
+        createFileService("Class1", snake, unicorn, sheep);
+    }
+
+    public List<FileServiceInfo> getFileServicesForClasses(
+            List<String> serviceClasses) {
+        return mIdToServiceInfo.values().stream()
+                .filter((info) -> serviceClasses.contains(info.getClassName()))
+                .collect(Collectors.toList());
+    }
+
+    public FileServiceInfo getFileServiceInfoForId(String serviceId) {
+        return mIdToServiceInfo.getOrDefault(serviceId, null);
+    }
+
+    public int getResourceForFileUri(Uri uri) {
+        return mFileUriToResource.getOrDefault(uri, 0);
+    }
+
+    private void createFileService(String className, Uri... filesIncluded) {
+        sServiceIdCounter++;
+        String id = "FileServiceId[" + sServiceIdCounter + "]";
+        List<Locale> locales = new ArrayList<Locale>(2) {{
+            add(Locale.US);
+            add(Locale.UK);
+        }};
+        Map<Locale, String> localeDict = new HashMap<Locale, String>() {{
+            put(Locale.US, "File Source " + sServiceIdCounter);
+            put(Locale.UK, "File Source with extra vowels " + sServiceIdCounter);
+        }};
+        List<FileInfo> fileInfos = Arrays.stream(filesIncluded)
+                .map(this::getFileInfoForUri)
+                .collect(Collectors.toList());
+        FileServiceInfo info = new FileServiceInfo(localeDict, className, locales,
+                id, new Date(System.currentTimeMillis() - 10000),
+                new Date(System.currentTimeMillis() + 10000),
+                fileInfos);
+        mIdToServiceInfo.put(id, info);
+    }
+
+    private Uri initFile(String relPath, int resource) {
+        Uri uri = new Uri.Builder()
+                .scheme(FILE_DOWNLOAD_SCHEME)
+                .authority(FILE_AUTHORITY)
+                .path(relPath)
+                .build();
+        mFileUriToResource.put(uri, resource);
+        return uri;
+    }
+
+    private FileInfo getFileInfoForUri(Uri uri) {
+        if (!mFileUriToResource.containsKey(uri)) {
+            return null;
+        }
+
+        InputStream fileIn = mContext.getResources().openRawResource(mFileUriToResource.get(uri));
+        int fileSize;
+        byte[] buffer;
+        byte[] md5Sum;
+        try {
+            fileSize = fileIn.available();
+            buffer = new byte[fileIn.available()];
+            fileIn.read(buffer);
+        } catch (IOException e) {
+            // ignore and just return null
+            return null;
+        }
+        try {
+            md5Sum = MessageDigest.getInstance("MD5").digest(buffer);
+        } catch (NoSuchAlgorithmException e) {
+            return null;
+        }
+        return new FileInfo(uri, "application/octet-stream", fileSize, md5Sum);
+    }
+}
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamingAppIdentifier.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FrontendAppIdentifier.java
similarity index 90%
rename from testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamingAppIdentifier.java
rename to testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FrontendAppIdentifier.java
index 7cbb14a..4fb46aa 100644
--- a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamingAppIdentifier.java
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/FrontendAppIdentifier.java
@@ -16,12 +16,12 @@
 
 package com.android.phone.testapps.embmsmw;
 
-public class StreamingAppIdentifier {
+public class FrontendAppIdentifier {
     private final int uid;
     private final String appName;
     private final int subscriptionId;
 
-    public StreamingAppIdentifier(int uid, String appName, int subscriptionId) {
+    public FrontendAppIdentifier(int uid, String appName, int subscriptionId) {
         this.uid = uid;
         this.appName = appName;
         this.subscriptionId = subscriptionId;
@@ -48,7 +48,7 @@
             return false;
         }
 
-        StreamingAppIdentifier that = (StreamingAppIdentifier) o;
+        FrontendAppIdentifier that = (FrontendAppIdentifier) o;
 
         if (uid != that.uid) {
             return false;
diff --git a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamStateTracker.java b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamStateTracker.java
index e340b11..bb9494d 100644
--- a/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamStateTracker.java
+++ b/testapps/EmbmsServiceTestApp/src/com/android/phone/testapps/embmsmw/StreamStateTracker.java
@@ -27,10 +27,10 @@
 public class StreamStateTracker {
     private static final String LOG_TAG = "MbmsStreamStateTracker";
 
-    private static final Map<StreamingAppIdentifier, AppActiveStreams>
+    private static final Map<FrontendAppIdentifier, AppActiveStreams>
             sPerAppStreamStates = new HashMap<>();
 
-    public static int getStreamingState(StreamingAppIdentifier appIdentifier, String serviceId) {
+    public static int getStreamingState(FrontendAppIdentifier appIdentifier, String serviceId) {
         AppActiveStreams appStreams = sPerAppStreamStates.get(appIdentifier);
         if (appStreams == null) {
             return StreamingService.STATE_STOPPED;
@@ -38,7 +38,7 @@
         return appStreams.getStateForService(serviceId);
     }
 
-    public static void startStreaming(StreamingAppIdentifier appIdentifier, String serviceId,
+    public static void startStreaming(FrontendAppIdentifier appIdentifier, String serviceId,
             IStreamingServiceCallback callback) {
         AppActiveStreams appStreams = sPerAppStreamStates.get(appIdentifier);
         if (appStreams == null) {
@@ -49,7 +49,7 @@
         appStreams.startStreaming(serviceId, callback);
     }
 
-    public static void stopStreaming(StreamingAppIdentifier appIdentifier, String serviceId) {
+    public static void stopStreaming(FrontendAppIdentifier appIdentifier, String serviceId) {
         Log.i(LOG_TAG, "Stopping stream " + serviceId);
         AppActiveStreams appStreams = sPerAppStreamStates.get(appIdentifier);
         if (appStreams == null) {
@@ -59,7 +59,7 @@
         appStreams.stopStreaming(serviceId);
     }
 
-    public static void dispose(StreamingAppIdentifier appIdentifier, String serviceId) {
+    public static void dispose(FrontendAppIdentifier appIdentifier, String serviceId) {
         AppActiveStreams appStreams = sPerAppStreamStates.get(appIdentifier);
         if (appStreams == null) {
             // We have no record of this app, so we can just move on.
@@ -68,7 +68,7 @@
         appStreams.dispose(serviceId);
     }
 
-    public static void disposeAll(StreamingAppIdentifier appIdentifier) {
+    public static void disposeAll(FrontendAppIdentifier appIdentifier) {
         sPerAppStreamStates.remove(appIdentifier);
     }
 
diff --git a/testapps/EmbmsTestDownloadApp/Android.mk b/testapps/EmbmsTestDownloadApp/Android.mk
index 66ca25b..080e5b0 100644
--- a/testapps/EmbmsTestDownloadApp/Android.mk
+++ b/testapps/EmbmsTestDownloadApp/Android.mk
@@ -3,6 +3,10 @@
 # Build the Sample Embms Download frontend
 include $(CLEAR_VARS)
 
+LOCAL_STATIC_JAVA_LIBRARIES := \
+        android-support-v7-recyclerview \
+        android-support-v4
+
 src_dirs := src
 res_dirs := res
 
diff --git a/testapps/EmbmsTestDownloadApp/res/layout/activity_main.xml b/testapps/EmbmsTestDownloadApp/res/layout/activity_main.xml
index 092a08a..07c7d37 100644
--- a/testapps/EmbmsTestDownloadApp/res/layout/activity_main.xml
+++ b/testapps/EmbmsTestDownloadApp/res/layout/activity_main.xml
@@ -25,17 +25,27 @@
         android:id="@+id/progress_window"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"/>
-    <ImageView
-        android:id="@+id/sample_picture"
-        android:layout_width="wrap_content"
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/downloaded_images"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_gravity="center"/>
-
+        android:scrollbars="horizontal"
+        android:horizontalSpacing="10dp"
+        android:gravity="center"/>
     <Button
         android:id="@+id/bind_button"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/bind_button" />
+    <Button
+        android:id="@+id/get_file_services_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/get_file_services_button" />
+    <Spinner
+        android:id="@+id/available_file_services"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
 
     <Button
         android:id="@+id/request_dl_button"
diff --git a/testapps/EmbmsTestDownloadApp/res/values/donottranslate_strings.xml b/testapps/EmbmsTestDownloadApp/res/values/donottranslate_strings.xml
index 4683fa2..a29aea3 100644
--- a/testapps/EmbmsTestDownloadApp/res/values/donottranslate_strings.xml
+++ b/testapps/EmbmsTestDownloadApp/res/values/donottranslate_strings.xml
@@ -18,4 +18,5 @@
 <resources>
     <string name="bind_button">Bind</string>
     <string name="request_dl_button">Request DL</string>
+    <string name="get_file_services_button">Fetch file services</string>
 </resources>
\ No newline at end of file
diff --git a/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/DownloadCompletionReceiver.java b/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/DownloadCompletionReceiver.java
index b4cf1d4..ef9e672 100644
--- a/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/DownloadCompletionReceiver.java
+++ b/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/DownloadCompletionReceiver.java
@@ -19,12 +19,21 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
+import android.telephony.MbmsDownloadManager;
 
 public class DownloadCompletionReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
         if (EmbmsTestDownloadApp.DOWNLOAD_DONE_ACTION.equals(intent.getAction())) {
-            EmbmsTestDownloadApp.getInstance().onDownloadDone();
+            int result = intent.getIntExtra(MbmsDownloadManager.EXTRA_RESULT,
+                    MbmsDownloadManager.RESULT_CANCELLED);
+            if (result != MbmsDownloadManager.RESULT_SUCCESSFUL) {
+                EmbmsTestDownloadApp.getInstance().onDownloadFailed(result);
+            }
+            Uri completedFile = intent.getParcelableExtra(
+                    MbmsDownloadManager.EXTRA_COMPLETED_FILE_URI);
+            EmbmsTestDownloadApp.getInstance().onDownloadDone(completedFile);
         }
     }
 }
diff --git a/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/EmbmsTestDownloadApp.java b/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/EmbmsTestDownloadApp.java
index 51e3a66..b7d3e01 100644
--- a/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/EmbmsTestDownloadApp.java
+++ b/testapps/EmbmsTestDownloadApp/src/com/android/phone/testapps/embmsdownload/EmbmsTestDownloadApp.java
@@ -17,41 +17,142 @@
 package com.android.phone.testapps.embmsdownload;
 
 import android.app.Activity;
-import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
 import android.telephony.MbmsDownloadManager;
+import android.telephony.SubscriptionManager;
 import android.telephony.mbms.DownloadCallback;
 import android.telephony.mbms.DownloadRequest;
+import android.telephony.mbms.FileServiceInfo;
 import android.telephony.mbms.MbmsDownloadManagerCallback;
 import android.telephony.mbms.MbmsException;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 public class EmbmsTestDownloadApp extends Activity {
+    private static final String LOG_TAG = "EmbmsDownloadApp";
+
     public static final String DOWNLOAD_DONE_ACTION =
             "com.android.phone.testapps.embmsdownload.DOWNLOAD_DONE";
 
-    private static final String TRIGGER_DOWNLOAD_ACTION =
-            "com.android.phone.testapps.embmsmw.TRIGGER_DOWNLOAD";
-    private static final String EXTRA_DOWNLOAD_REQUEST =
-            "com.android.phone.testapps.embmsmw.EXTRA_DOWNLOAD_REQUEST";
     private static final String APP_NAME = "SampleAppName";
+    private static final String CUSTOM_EMBMS_TEMP_FILE_LOCATION = "customEmbmsTempFiles";
+
+    private static final String FILE_AUTHORITY = "com.android.phone.testapps";
+    private static final String FILE_DOWNLOAD_SCHEME = "filedownload";
 
     private static EmbmsTestDownloadApp sInstance;
 
-    private MbmsDownloadManagerCallback mCallback = new MbmsDownloadManagerCallback() {};
+    private static final class ImageAdapter
+            extends RecyclerView.Adapter<ImageAdapter.ImageViewHolder> {
+        static class ImageViewHolder extends RecyclerView.ViewHolder {
+            public ImageView imageView;
+            public ImageViewHolder(ImageView view) {
+                super(view);
+                imageView = view;
+            }
+        }
+
+        private final List<Uri> mImageUris = new ArrayList<>();
+
+        @Override
+        public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+            ImageView view = new ImageView(parent.getContext());
+            view.setAdjustViewBounds(true);
+            view.setMaxHeight(500);
+            return new ImageViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(ImageViewHolder holder, int position) {
+            holder.imageView.setImageURI(mImageUris.get(position));
+        }
+
+        @Override
+        public int getItemCount() {
+            return mImageUris.size();
+        }
+
+        public void addImage(Uri uri) {
+            mImageUris.add(uri);
+            notifyDataSetChanged();
+        }
+    }
+
+    private final class FileServiceInfoAdapter
+            extends ArrayAdapter<FileServiceInfo> {
+        public FileServiceInfoAdapter(Context context) {
+            super(context, android.R.layout.simple_spinner_item);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            FileServiceInfo info = getItem(position);
+            TextView result = new TextView(EmbmsTestDownloadApp.this);
+            result.setText(info.getNames().get(info.getLocales().get(0)));
+            return result;
+        }
+
+        @Override
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            FileServiceInfo info = getItem(position);
+            TextView result = new TextView(EmbmsTestDownloadApp.this);
+            String text = "name="
+                    + info.getNames().get(info.getLocales().get(0))
+                    + ", "
+                    + "numFiles="
+                    + info.getFiles().size();
+            result.setText(text);
+            return result;
+        }
+
+        public void update(List<FileServiceInfo> services) {
+            clear();
+            addAll(services);
+        }
+    }
+
+    private MbmsDownloadManagerCallback mCallback = new MbmsDownloadManagerCallback() {
+        @Override
+        public void error(int errorCode, String message) {
+            runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this,
+                    "Error " + errorCode + ": " + message, Toast.LENGTH_SHORT).show());
+        }
+
+        @Override
+        public void fileServicesUpdated(List<FileServiceInfo> services) {
+            EmbmsTestDownloadApp.this.runOnUiThread(() ->
+                    Toast.makeText(EmbmsTestDownloadApp.this,
+                            "Got services length " + services.size(),
+                            Toast.LENGTH_SHORT).show());
+            updateFileServicesList(services);
+        }
+    };
 
     private MbmsDownloadManager mDownloadManager;
     private Handler mHandler;
     private HandlerThread mHandlerThread;
+    private FileServiceInfoAdapter mFileServiceInfoAdapter;
+    private ImageAdapter mImageAdapter;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -62,38 +163,61 @@
         mHandlerThread = new HandlerThread("EmbmsDownloadWorker");
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
+        mFileServiceInfoAdapter = new FileServiceInfoAdapter(this);
 
-        File destination = null;
-        try {
-            destination = new File(getFilesDir(), "image.png").getCanonicalFile();
-        } catch (IOException e) {
-            // ignore, this is temp code
-        }
-
-        Intent completionIntent = new Intent(DOWNLOAD_DONE_ACTION);
-        completionIntent.setClass(this, DownloadCompletionReceiver.class);
-
-        DownloadRequest request = new DownloadRequest.Builder()
-                .setId(0)
-                .setServiceInfo(null) // TODO: this isn't supposed to be null, but not yet used
-                .setSource(null) // ditto
-                .setDest(Uri.fromFile(destination))
-                .setAppIntent(completionIntent)
-                .build();
+        RecyclerView downloadedImages = (RecyclerView) findViewById(R.id.downloaded_images);
+        downloadedImages.setLayoutManager(
+                new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
+        mImageAdapter = new ImageAdapter();
+        downloadedImages.setAdapter(mImageAdapter);
 
         Button bindButton = (Button) findViewById(R.id.bind_button);
         bindButton.setOnClickListener((view) -> mHandler.post(() -> {
             try {
                 mDownloadManager = MbmsDownloadManager.createManager(this, mCallback, APP_NAME);
+                File downloadDir = new File(EmbmsTestDownloadApp.this.getFilesDir(),
+                        CUSTOM_EMBMS_TEMP_FILE_LOCATION);
+                downloadDir.mkdirs();
+                mDownloadManager.setTempFileRootDirectory(downloadDir);
+                runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this,
+                        "Initialization done", Toast.LENGTH_SHORT).show());
             } catch (MbmsException e) {
-                Toast.makeText(EmbmsTestDownloadApp.this,
-                        "caught MbmsException: " + e.getErrorCode(), Toast.LENGTH_SHORT).show();
+                runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this,
+                        "caught MbmsException: " + e.getErrorCode(), Toast.LENGTH_SHORT).show());
             }
         }));
 
+        Button getFileServicesButton = (Button) findViewById(R.id.get_file_services_button);
+        getFileServicesButton.setOnClickListener((view) -> mHandler.post(() -> {
+            try {
+                mDownloadManager.getFileServices(Collections.singletonList("Class1"));
+            } catch (MbmsException e) {
+                runOnUiThread(() -> Toast.makeText(EmbmsTestDownloadApp.this,
+                        "caught MbmsException: " + e.getErrorCode(), Toast.LENGTH_SHORT).show());
+            }
+        }));
+
+        final Spinner serviceSelector = (Spinner) findViewById(R.id.available_file_services);
+        mFileServiceInfoAdapter.setDropDownViewResource(
+                android.R.layout.simple_spinner_dropdown_item);
+        serviceSelector.setAdapter(mFileServiceInfoAdapter);
+
         Button requestDlButton = (Button) findViewById(R.id.request_dl_button);
         requestDlButton.setOnClickListener((view) ->  {
-            mDownloadManager.download(request, new DownloadCallback());
+            if (mDownloadManager == null) {
+                Toast.makeText(EmbmsTestDownloadApp.this,
+                        "No download service bound", Toast.LENGTH_SHORT).show();
+                return;
+            }
+            FileServiceInfo serviceInfo =
+                    (FileServiceInfo) serviceSelector.getSelectedItem();
+            if (serviceInfo == null) {
+                Toast.makeText(EmbmsTestDownloadApp.this,
+                        "No file service selected", Toast.LENGTH_SHORT).show();
+                return;
+            }
+
+            performDownload(serviceInfo);
         });
     }
 
@@ -108,15 +232,64 @@
         return sInstance;
     }
 
+    public void onDownloadFailed(int result) {
+        runOnUiThread(() ->
+                Toast.makeText(this, "Download failed: " + result, Toast.LENGTH_SHORT).show());
+    }
+
     // TODO: assumes that process does not get killed. Replace with more robust alternative
-    public void onDownloadDone() {
-        ImageView picture = (ImageView) findViewById(R.id.sample_picture);
-        File imageFile = new File(getFilesDir(), "image.png");
+    public void onDownloadDone(Uri fileLocation) {
+        Log.i(LOG_TAG, "File completed: " + fileLocation);
+        File imageFile = new File(fileLocation.getPath());
         if (!imageFile.exists()) {
             Toast.makeText(this, "Download done but destination doesn't exist", Toast.LENGTH_SHORT)
                     .show();
             return;
         }
-        runOnUiThread(() -> picture.setImageURI(Uri.fromFile(imageFile)));
+        mImageAdapter.addImage(fileLocation);
+    }
+
+    private void updateFileServicesList(List<FileServiceInfo> services) {
+        runOnUiThread(() -> mFileServiceInfoAdapter.update(services));
+    }
+
+    private void performDownload(FileServiceInfo info) {
+        File destination = null;
+        try {
+            if (info.getFiles().size() > 1) {
+                destination = new File(getFilesDir(), "images/").getCanonicalFile();
+                destination.mkdirs();
+            } else {
+                destination = new File(getFilesDir(), "images/image.png").getCanonicalFile();
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+
+        Intent completionIntent = new Intent(DOWNLOAD_DONE_ACTION);
+        completionIntent.setClass(this, DownloadCompletionReceiver.class);
+
+        Uri sourceUri = new Uri.Builder()
+                .scheme(FILE_DOWNLOAD_SCHEME)
+                .authority(FILE_AUTHORITY)
+                .path("/")
+                .build();
+
+        DownloadRequest request = new DownloadRequest.Builder()
+                .setId(0)
+                .setServiceInfo(info)
+                .setSource(sourceUri)
+                .setDest(Uri.fromFile(destination))
+                .setAppIntent(completionIntent)
+                .setSubscriptionId(SubscriptionManager.getDefaultSubscriptionId())
+                .build();
+
+        try {
+            mDownloadManager.download(request, null);
+        } catch (MbmsException e) {
+            Toast.makeText(EmbmsTestDownloadApp.this,
+                    "caught MbmsException: " + e.getErrorCode(), Toast.LENGTH_SHORT).show();
+        }
+
     }
 }