Files
frameworks_base/telephony/java/android/telephony/MbmsDownloadManager.java
Hall Liu 727a05b23e Add callback for initialization done
Add callback for initialization done in the framework, and listen to it
in the testapps. Make initialization asynchronous as well for both
download and streaming.

Change-Id: Iea7f803df9d2752401b2eca9f6c7375007cac35e
2017-06-16 18:58:57 -07:00

522 lines
22 KiB
Java

/*
* Copyright (C) 2016 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;
import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
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.MbmsDownloadManagerCallback;
import android.telephony.mbms.MbmsDownloadReceiver;
import android.telephony.mbms.MbmsException;
import android.telephony.mbms.MbmsTempFileProvider;
import android.telephony.mbms.MbmsUtils;
import android.telephony.mbms.vendor.IMbmsDownloadService;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
/** @hide */
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
* {@link #EXTRA_RESULT}
* {@link #EXTRA_FILE_INFO}
* {@link #EXTRA_REQUEST}
* {@link #EXTRA_TEMP_LIST}
* {@link #EXTRA_FINAL_URI}
*
* TODO: future systemapi
*/
public static final String ACTION_DOWNLOAD_RESULT_INTERNAL =
"android.telephony.mbms.action.DOWNLOAD_RESULT_INTERNAL";
/**
* The MBMS middleware should send this when it wishes to request {@code content://} URIs to
* serve as temp files for downloads or when it wishes to resume paused downloads. Mandatory
* extras are
* {@link #EXTRA_REQUEST}
*
* Optional extras are
* {@link #EXTRA_FD_COUNT} (0 if not present)
* {@link #EXTRA_PAUSED_LIST} (empty if not present)
*
* TODO: future systemapi
*/
public static final String ACTION_FILE_DESCRIPTOR_REQUEST =
"android.telephony.mbms.action.FILE_DESCRIPTOR_REQUEST";
/**
* The MBMS middleware should send this when it wishes to clean up temp files in the app's
* filesystem. Mandatory extras are:
* {@link #EXTRA_TEMP_FILES_IN_USE}
*
* TODO: future systemapi
*/
public static final String ACTION_CLEANUP =
"android.telephony.mbms.action.CLEANUP";
/**
* 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_FILE_INFO = "android.telephony.mbms.extra.FILE_INFO";
/**
* Extra containing the {@link DownloadRequest} for which the download result or file
* descriptor request is for. Must not be null.
*/
public static final String EXTRA_REQUEST = "android.telephony.mbms.extra.REQUEST";
/**
* Extra containing a {@link List} of {@link Uri}s that were used as temp files for this
* completed file. These {@link Uri}s should have scheme {@code file://}, and the temp
* files will be deleted upon receipt of the intent.
* May be null.
*/
public static final String EXTRA_TEMP_LIST = "android.telephony.mbms.extra.TEMP_LIST";
/**
* Extra containing a single {@link Uri} indicating the path to the temp file in which the
* decoded downloaded file resides. Must not be null.
*/
public static final String EXTRA_FINAL_URI = "android.telephony.mbms.extra.FINAL_URI";
/**
* Extra containing an integer indicating the number of temp files requested.
*/
public static final String EXTRA_FD_COUNT = "android.telephony.mbms.extra.FD_COUNT";
/**
* Extra containing a list of {@link Uri}s that the middleware is requesting access to via
* {@link #ACTION_FILE_DESCRIPTOR_REQUEST} in order to resume downloading. These {@link Uri}s
* should have scheme {@code file://}.
*/
public static final String EXTRA_PAUSED_LIST = "android.telephony.mbms.extra.PAUSED_LIST";
/**
* Extra containing a list of {@link android.telephony.mbms.UriPathPair}s, used in the
* response to {@link #ACTION_FILE_DESCRIPTOR_REQUEST}. These are temp files that are meant
* to be used for new file downloads.
*/
public static final String EXTRA_FREE_URI_LIST = "android.telephony.mbms.extra.FREE_URI_LIST";
/**
* Extra containing a list of {@link android.telephony.mbms.UriPathPair}s, used in the
* response to {@link #ACTION_FILE_DESCRIPTOR_REQUEST}. These
* {@link android.telephony.mbms.UriPathPair}s contain {@code content://} URIs that provide
* access to previously paused downloads.
*/
public static final String EXTRA_PAUSED_URI_LIST =
"android.telephony.mbms.extra.PAUSED_URI_LIST";
/**
* Extra containing a string that points to the middleware's knowledge of where the temp file
* root for the app is. The path should be a canonical path as returned by
* {@link File#getCanonicalPath()}
*/
public static final String EXTRA_TEMP_FILE_ROOT =
"android.telephony.mbms.extra.TEMP_FILE_ROOT";
/**
* Extra containing a list of {@link Uri}s indicating temp files which the middleware is
* still using.
*/
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 final Context mContext;
private int mSubscriptionId = INVALID_SUBSCRIPTION_ID;
private AtomicReference<IMbmsDownloadService> mService = new AtomicReference<>(null);
private final IMbmsDownloadManagerCallback mCallback;
private final String mDownloadAppName;
private MbmsDownloadManager(Context context, IMbmsDownloadManagerCallback callback,
String downloadAppName, int subId) {
mContext = context;
mCallback = callback;
mDownloadAppName = downloadAppName;
mSubscriptionId = subId;
}
/**
* Create a new MbmsDownloadManager using the system default data subscription ID.
* See {@link #create(Context, IMbmsDownloadManagerCallback, String, int)}
*
* @hide
*/
public static MbmsDownloadManager create(Context context,
IMbmsDownloadManagerCallback listener, String downloadAppName)
throws MbmsException {
return create(context, listener, downloadAppName,
SubscriptionManager.getDefaultSubscriptionId());
}
/**
* Create a new MbmsDownloadManager using the given subscription ID.
*
* Note that this call will bind a remote service and that may take a bit. The instance of
* {@link MbmsDownloadManager} that is returned will not be ready for use until
* {@link IMbmsDownloadManagerCallback#middlewareReady()} is called on the provided callback.
* If you attempt to use the manager before it is ready, a {@link MbmsException} will be thrown.
*
* This also may throw an {@link IllegalArgumentException} or a {@link MbmsException}.
*
* @param context The instance of {@link Context} to use
* @param listener A callback to get asynchronous error messages and file service updates.
* @param downloadAppName The app name, as negotiated with the eMBMS provider
* @param subscriptionId The data subscription ID to use
* @hide
*/
public static MbmsDownloadManager create(Context context,
IMbmsDownloadManagerCallback listener, String downloadAppName, int subscriptionId)
throws MbmsException {
MbmsDownloadManager mdm = new MbmsDownloadManager(context, listener, downloadAppName,
subscriptionId);
mdm.bindAndInitialize();
return mdm;
}
private void bindAndInitialize() throws MbmsException {
MbmsUtils.startBinding(mContext, MBMS_DOWNLOAD_SERVICE_ACTION,
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IMbmsDownloadService downloadService =
IMbmsDownloadService.Stub.asInterface(service);
try {
downloadService.initialize(
mDownloadAppName, mSubscriptionId, mCallback);
} catch (RemoteException e) {
Log.e(LOG_TAG, "Service died before initialization");
return;
}
mService.set(downloadService);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mService.set(null);
}
});
}
/**
* An inspection API to retrieve the list of available
* {@link android.telephony.mbms.FileServiceInfo}s currently being advertised.
* The results are returned asynchronously via a call to
* {@link IMbmsDownloadManagerCallback#fileServicesUpdated(List)}
*
* The serviceClasses argument lets the app filter on types of programming and is opaque data
* negotiated beforehand between the app and the carrier.
*
* Multiple calls replace the list of serviceClasses of interest.
*
* This may throw an {@link MbmsException} containing one of the following errors:
* {@link MbmsException#ERROR_MIDDLEWARE_NOT_BOUND}
* {@link MbmsException#ERROR_CONCURRENT_SERVICE_LIMIT_REACHED}
* {@link MbmsException#ERROR_SERVICE_LOST}
*
* Asynchronous error codes via the {@link MbmsDownloadManagerCallback#error(int, String)}
* callback can include any of the errors except:
* {@link MbmsException#ERROR_UNABLE_TO_START_SERVICE}
* {@link MbmsException#ERROR_END_OF_SESSION}
*/
public void getFileServices(List<String> classList) throws MbmsException {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND);
}
try {
int returnCode = downloadService.getFileServices(
mDownloadAppName, mSubscriptionId, classList);
if (returnCode != MbmsException.SUCCESS) {
throw new MbmsException(returnCode);
}
} catch (RemoteException e) {
Log.w(LOG_TAG, "Remote process died");
mService.set(null);
throw new MbmsException(MbmsException.ERROR_SERVICE_LOST);
}
}
/**
* Sets the temp file root for downloads.
* All temp files created for the middleware to write to will be contained in the specified
* directory. Applications that wish to specify a location only need to call this method once
* as long their data is persisted in storage -- the argument will be stored both in a
* local instance of {@link android.content.SharedPreferences} and by the middleware.
*
* If this method is not called at least once before calling
* {@link #download(DownloadRequest, IDownloadCallback)}, the framework
* will default to a directory formed by the concatenation of the app's files directory and
* {@link android.telephony.mbms.MbmsTempFileProvider#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY}.
*
* This method may not be called while any download requests are still active. If this is
* the case, an {@link MbmsException} will be thrown with code
* {@link MbmsException#ERROR_CANNOT_CHANGE_TEMP_FILE_ROOT}
*
* The {@link File} supplied as a root temp file directory must already exist. If not, an
* {@link IllegalArgumentException} will be thrown.
* @param tempFileRootDirectory A directory to place temp files in.
*/
public void setTempFileRootDirectory(@NonNull File tempFileRootDirectory)
throws MbmsException {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND);
}
if (!tempFileRootDirectory.exists()) {
throw new IllegalArgumentException("Provided directory does not exist");
}
if (!tempFileRootDirectory.isDirectory()) {
throw new IllegalArgumentException("Provided File is not a directory");
}
String filePath;
try {
filePath = tempFileRootDirectory.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Unable to canonicalize the provided path: " + e);
}
try {
int result = downloadService.setTempFileRootDirectory(
mDownloadAppName, mSubscriptionId, filePath);
if (result != MbmsException.SUCCESS) {
throw new MbmsException(result);
}
} catch (RemoteException e) {
mService.set(null);
throw new MbmsException(MbmsException.ERROR_SERVICE_LOST);
}
SharedPreferences prefs = mContext.getSharedPreferences(
MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
prefs.edit().putString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, filePath).apply();
}
/**
* Requests a download of a file that is available via multicast.
*
* downloadListener is an optional callback object which can be used to get progress reports
* of a currently occuring download. Note this can only run while the calling app
* is running, so future downloads will simply result in resultIntents being sent
* for completed or errored-out downloads. A NULL indicates no callbacks are needed.
*
* May throw an {@link IllegalArgumentException}
*
* If {@link #setTempFileRootDirectory(File)} has not called after the app has been installed,
* this method will create a directory at the default location defined at
* {@link MbmsTempFileProvider#DEFAULT_TOP_LEVEL_TEMP_DIRECTORY} and store that as the temp
* file root directory.
*
* Asynchronous errors through the listener include any of the errors
*
* @param request The request that specifies what should be downloaded
* @param callback Optional callback that will provide progress updates if the app is running.
*/
public void download(DownloadRequest request, IDownloadCallback callback)
throws MbmsException {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
throw new MbmsException(MbmsException.ERROR_MIDDLEWARE_NOT_BOUND);
}
// Check to see whether the app's set a temp root dir yet, and set it if not.
SharedPreferences prefs = mContext.getSharedPreferences(
MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_FILE_NAME, 0);
if (prefs.getString(MbmsTempFileProvider.TEMP_FILE_ROOT_PREF_NAME, null) == null) {
File tempRootDirectory = new File(mContext.getFilesDir(),
MbmsTempFileProvider.DEFAULT_TOP_LEVEL_TEMP_DIRECTORY);
tempRootDirectory.mkdirs();
setTempFileRootDirectory(tempRootDirectory);
}
request.setAppName(mDownloadAppName);
// Check if the request is a multipart download. If so, validate that the destination is
// a directory that exists.
// TODO: figure out what qualifies a request as a multipart download request.
if (request.getSourceUri().getLastPathSegment() != null &&
request.getSourceUri().getLastPathSegment().contains("*")) {
File toFile = new File(request.getDestinationUri().getSchemeSpecificPart());
if (!toFile.isDirectory()) {
throw new IllegalArgumentException("Multipart download must specify valid " +
"destination directory.");
}
}
// TODO: check to make sure destination is clear
// TODO: write download request token
try {
downloadService.download(request, callback);
} catch (RemoteException e) {
mService.set(null);
}
}
/**
* Returns a list DownloadRequests that originated from this application (UID).
*
* May throw a RemoteException.
*
* Asynchronous errors through the listener include any of the errors except
* <li>ERROR_UNABLED_TO_START_SERVICE</li>
* <li>ERROR_MSDC_INVALID_SERVICE_ID</li>
* <li>ERROR_MSDC_END_OF_SESSION</li>
*/
public List<DownloadRequest> listPendingDownloads() {
return null;
}
/**
* Attempts to cancel the specified DownloadRequest.
*
* May throw a RemoteException.
*
* Synchronous responses may include
* <li>SUCCESS</li>
* <li>ERROR_MSDC_CONCURRENT_SERVICE_LIMIT_REACHED</li>
* <li>ERROR_MSDC_UNKNOWN_REQUEST</li>
*/
public int cancelDownload(DownloadRequest downloadRequest) {
return 0;
}
/**
* Gets information about current and known upcoming downloads.
*
* Current is a straightforward count of the files being downloaded "now"
* for some definition of now (may be racey).
* Future downloads include counts of files with pending repair operations, counts of
* files with future downloads and indication of scheduled download times with unknown
* file details.
*
* May throw an IllegalArgumentException or RemoteException.
*
* If the DownloadRequest is unknown the results will be null.
*/
public DownloadStatus getDownloadStatus(DownloadRequest downloadRequest) {
return null;
}
/**
* Resets middleware knowledge regarding this download request.
*
* This state consists of knowledge of what files have already been downloaded.
* Normally the middleware won't download files who's hash matches previously downloaded
* content, even if that content has since been deleted. If this function is called
* repeated content will be downloaded again when available. This does not interrupt
* in-progress downloads.
*
* May throw an IllegalArgumentException or RemoteException.
*
* <li>SUCCESS</li>
* <li>ERROR_MSDC_CONCURRENT_SERVICE_LIMIT_REACHED</li>
* <li>ERROR_MSDC_UNKNOWN_REQUEST</li>
*/
public int resetDownloadKnowledge(DownloadRequest downloadRequest) {
return 0;
}
/**
* Retrieves the {@link ComponentName} for the {@link android.content.BroadcastReceiver} that
* the various intents from the middleware should be targeted towards.
* @param uid The uid of the frontend app.
* @return The component name of the receiver that the middleware should send its intents to,
* or null if the app didn't declare it in the manifest.
*
* @hide
* future systemapi
*/
public static ComponentName getAppReceiverFromUid(Context context, int uid) {
String[] packageNames = context.getPackageManager().getPackagesForUid(uid);
if (packageNames == null) {
return null;
}
for (String packageName : packageNames) {
ComponentName candidate = new ComponentName(packageName,
MbmsDownloadReceiver.class.getCanonicalName());
Intent queryIntent = new Intent();
queryIntent.setComponent(candidate);
List<ResolveInfo> receivers =
context.getPackageManager().queryBroadcastReceivers(queryIntent, 0);
if (receivers != null && receivers.size() > 0) {
return candidate;
}
}
return null;
}
public void dispose() {
try {
IMbmsDownloadService downloadService = mService.get();
if (downloadService == null) {
Log.i(LOG_TAG, "Service already dead");
return;
}
downloadService.dispose(mDownloadAppName, mSubscriptionId);
mService.set(null);
} catch (RemoteException e) {
// Ignore
Log.i(LOG_TAG, "Remote exception while disposing of service");
}
}
}