项目本来开始使用的是友盟的自动提示更新功能,现在由于应用市场,系统厂商,运营商等多方面对友盟自动更新服务的限制,友盟将于2016年10月份停止所有应用的自动更新服务,这就让我倒霉了,得自己在客户端写自动更新的功能,目前所用到的是Android中DownLoadManager。
DownLoadManager也不是什么新鲜玩意了,从Android2.3(API level 9)开始Android用系统服务(Service)的方式提供了Download Manager来优化处理长时间的下载操作。Download Manager处理HTTP连接并监控连接中的状态变化以及系统重启来确保每一个下载任务顺利完成。当然写这片文章也借鉴了不少网上的类似的文章,但是还是会有一些干货存在的,希望大家认真看下去,并且有什么不对的地方大家可以指出来。
在本文中,我将所有有关的操作全部写在一个DownLoadUtil类里面,并且附有很详细的注释。
import java.io.File;
import android.app.DownloadManager;
import android.app.DownloadManager.Request;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
/**
* 下载更新的util 主要是负责 版本的相关更新工作
* 实际 下载更新的具体步骤:
* 1.将自己应用的版本号传递给服务器 服务器与自己最新的app版本号对比(文件命名添加版本号的后缀)
* 如果服务器版本号>本地所传递过去的版本号 服务器传递版本号和URL地址过来 本地下载更新
* 将下载返回的ID存放在sharedPreference中
* 2.如果用户的不正当操作使得下载终止:A:检查数据库中下载的文件状态是否为200(成功)
* 如果成功就直接跳转到安装界面
* 如果不成功 就将remove(long... ids)当前下载任务remove掉 文件也删除 sp中也数据 清零开启新的下载任务
*
*/
public class DownLoadUtil {
private Context context;
private String url;
private String notificationTitle;
private String notificationDescription;
private DownloadManager downLoadManager;
public static final String DOWNLOAD_FOLDER_NAME = "app/apk/download";
public static final String DOWNLOAD_FILE_NAME = "test.apk";
public String getNotificationTitle() {
return notificationTitle;
}
public void setNotificationTitle(String notificationTitle) {
this.notificationTitle = notificationTitle;
}
public String getNotificationDescription() {
return notificationDescription;
}
public void setNotificationDescription(String notificationDescription) {
this.notificationDescription = notificationDescription;
}
public DownLoadUtil(Context context) {
this.context = context;
downLoadManager = (DownloadManager) this.context
.getSystemService(Context.DOWNLOAD_SERVICE);
}
//得到当前应用的版本号
public int getVersionName() throws Exception {
//getPackageName()是你当前类的包名,0代表是获取版本信息
PackageManager packageManager = context.getPackageManager();
PackageInfo packInfo = packageManager.getPackageInfo(context.getPackageName(),
0);
return packInfo.versionCode;
}
/**
* 服务端的版本号与客户端的版本号对比
* @param localVersion 本地版本号
* @param serverVersion 服务器版本号
* @return true 可以下载更新 false 不能下载更新
*/
public boolean canUpdate(int localVersion,int serverVersion){
if(localVersion <=0 || serverVersion<=0)
return false;
if(localVersion>=serverVersion){
return false;
}
return true;
}
public void downLoad(String url){
Request request=new Request(Uri.parse(url));
//设置状态栏中显示Notification
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if(!TextUtils.isEmpty(getNotificationTitle())){
request.setTitle(getNotificationTitle());
}
if(!TextUtils.isEmpty(getNotificationDescription())){
request.setDescription(getNotificationDescription());
}
//设置可用的网络类型
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);
//不显示下载界面
request.setVisibleInDownloadsUi(false);
//创建文件的下载路径
File folder = Environment.getExternalStoragePublicDirectory(DOWNLOAD_FOLDER_NAME);
if (!folder.exists() || !folder.isDirectory()) {
folder.mkdirs();
}
//指定下载的路径为和上面创建的路径相同
request.setDestinationInExternalPublicDir(DOWNLOAD_FOLDER_NAME, DOWNLOAD_FILE_NAME);
//设置文件类型
MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
String mimeString = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(url));
request.setMimeType(mimeString);
//将请求加入请求队列会 downLoadManager会自动调用对应的服务执行者个请求
downLoadManager.enqueue(request);
}
//文件的安装 方法
public static boolean install(Context context, String filePath) {
Intent intent = new Intent(Intent.ACTION_VIEW);
File file = new File(filePath);
if (file != null && file.length() > 0 && file.exists() && file.isFile()) {
intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return true;
}
return false;
}
}
在DownloadUtil中包括了请求的方法,比较版本号,安装最新版本的apk文件的方法。
代码59-65行中,定义了一个得到本地apk版本号的方法。
代码68-80行,比较本地版本号和服务器apk的版本号,返回是否需要更新的布尔值。在实际项目中,你的应用可能需要在你每次登陆进去的时候,访问网络请求,得到服务器的apk的最新版本号,与自己本地的版本号对比,来控制是否立即更新或者稍后再说的dialog的显示。
在代码的85-91行中,设置了下载文件是Notifiction的显示状况和样式。setNotificationVisibility用来控制什么时候显示notification甚至隐藏。主要有以下几个参数:
Request.VISIBILITY_VISIBLE:在下载进行的过程中,通知栏中会一直显示该下载的Notification,当下载完成时,该Notification会被移除,这是默认的参数值。
Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED:在下载过程中通知栏会一直显示该下载的Notification,在下载完成后该Notification会继续显示,直到用户点击该Notification或者消除该Notification。
Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION:只有在下载完成后该Notification才会被显示。
Request.VISIBILITY_HIDDEN:不显示该下载请求的Notification。如果要使用这个参数,需要在应用的清单文件中加上DOWNLOAD_WITHOUT_NOTIFICATION权限。
在代码中的110行将请求加入到请求队列中,如果文件下载完成,就会向系统发送一个广播。
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* 运用downloadManager下载完成之后 通知系统我下载完成
*
*/
public class DownLoadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)){
}
}
}
其实自己可以简单的定义一个广播接收者,来接收对应Action的广播。当我们接受到的广播的Action为ACTION_DOWNLOAD_COMPLETE的时候我们可以 进行最新版本的APK的安装操作,也就是上面DownloadUtil类中的install方法。
其实系统的DownLoadManager是用ContentProvider来实现的。看看上面DownUtil类中download方法中最后一句代码,downloadManager.enqueue(request)我们看源代码
public long enqueue(Request request) {
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
}
看到我们熟悉的代码mResolver.insert方法 只实际就是我们的访问contentProvider对象,可以去查看相应的URI参数public static final Uri CONTENT_URI =Uri.parse("content://downloads/my_downloads");这个正是,我们的系统源代码downloadProvider中的URI,DownloadProvicer源代码位于Android源码中的packages/providers下面 可以自行下载查看
static {
sURIMatcher.addURI("downloads", "my_downloads", MY_DOWNLOADS);
sURIMatcher.addURI("downloads", "my_downloads/#", MY_DOWNLOADS_ID);
sURIMatcher.addURI("downloads", "all_downloads", ALL_DOWNLOADS);
sURIMatcher.addURI("downloads", "all_downloads/#", ALL_DOWNLOADS_ID);
sURIMatcher.addURI("downloads",
"my_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads",
"all_downloads/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
REQUEST_HEADERS_URI);
// temporary, for backwards compatibility
sURIMatcher.addURI("downloads", "download", MY_DOWNLOADS);
sURIMatcher.addURI("downloads", "download/#", MY_DOWNLOADS_ID);
sURIMatcher.addURI("downloads",
"download/#/" + Downloads.Impl.RequestHeaders.URI_SEGMENT,
REQUEST_HEADERS_URI);
sURIMatcher.addURI("downloads",
Downloads.Impl.PUBLICLY_ACCESSIBLE_DOWNLOADS_URI_SEGMENT + "/#",
PUBLIC_DOWNLOAD_ID);
}
这样就可以访问我们的DownloadProvider了,我们仔细去查看downloadProvider中的insert方法
/**
* Inserts a row in the database
*/
@Override
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
// note we disallow inserting into ALL_DOWNLOADS
int match = sURIMatcher.match(uri);
if (match != MY_DOWNLOADS) {
Log.d(Constants.TAG, "calling insert on an unknown/invalid URI: " + uri);
throw new IllegalArgumentException("Unknown/Invalid URI " + uri);
}
//.......中间代码省略
Context context = getContext();
if (values.getAsInteger(Downloads.Impl.COLUMN_DESTINATION) ==
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD) {
// don't start downloadservice because it has nothing to do in this case.
// but does a completion notification need to be sent?
if (Downloads.Impl.isNotificationToBeDisplayed(vis)) {
DownloadNotification notifier = new DownloadNotification(context, mSystemFacade);
notifier.notificationForCompletedDownload(rowID,
values.getAsString(Downloads.Impl.COLUMN_TITLE),
Downloads.Impl.STATUS_SUCCESS,
Downloads.Impl.DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD,
lastMod);
}
} else {
context.startService(new Intent(context, DownloadService.class));
}
notifyContentChanged(uri, match);
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
代码中的18-29行其实简单得说就是检查我们数据库中是否有东西要下载有事可做 ,没有事情可做就执行else的操作,在else操作中启动了我们的DownloadService的服务,在跟进DownloadService,在DownloadService的onCreate方法中就进行了一些数据的初始化操作。我们看onStartCommend方法中的具体操作和实现
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
int returnValue = super.onStartCommand(intent, flags, startId);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onStart");
}
updateFromProvider();
return returnValue;
}
看这个方法最重要的方法也就是执行upDateFromProvider方法,再继续跟进
private void updateFromProvider() {
synchronized (this) {
mPendingUpdate = true;
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
mSystemFacade.startThread(mUpdateThread);
}
}
}
启动了一个UpdateThread的线程,是线程我们就来看看他的run方法都干了些什么。
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
long wakeUp = Long.MAX_VALUE;
for (;;) {
synchronized (DownloadService.this) {
if (mUpdateThread != this) {
throw new IllegalStateException(
"multiple UpdateThreads in DownloadService");
}
if (!mPendingUpdate) {
mUpdateThread = null;
if (!keepService) {
stopSelf();
}
if (wakeUp != Long.MAX_VALUE) {
scheduleAlarm(wakeUp);
}
return;
}
mPendingUpdate = false;
}
long now = mSystemFacade.currentTimeMillis();
boolean mustScan = false;
keepService = false;
wakeUp = Long.MAX_VALUE;
Set idsNoLongerInDatabase = new HashSet(mDownloads.keySet());
Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null, null, null, null);
if (cursor == null) {
continue;
}
try {
DownloadInfo.Reader reader =
new DownloadInfo.Reader(getContentResolver(), cursor);
int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
if (Constants.LOGVV) {
Log.i(Constants.TAG, "number of rows from downloads-db: " +
cursor.getCount());
}
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
long id = cursor.getLong(idColumn);
idsNoLongerInDatabase.remove(id);
DownloadInfo info = mDownloads.get(id);
if (info != null) {
updateDownload(reader, info, now);
} else {
info = insertDownload(reader, now);
}
if (info.shouldScanFile() && !scanFile(info, true, false)) {
mustScan = true;
keepService = true;
}
if (info.hasCompletionNotification()) {
keepService = true;
}
long next = info.nextAction(now);
if (next == 0) {
keepService = true;
} else if (next > 0 && next < wakeUp) {
wakeUp = next;
}
}
} finally {
cursor.close();
}
for (Long id : idsNoLongerInDatabase) {
deleteDownload(id);
}
// is there a need to start the DownloadService? yes, if there are rows to be
// deleted.
if (!mustScan) {
for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) {
mustScan = true;
keepService = true;
break;
}
}
}
mNotifier.updateNotification(mDownloads.values());
if (mustScan) {
bindMediaScanner();
} else {
mMediaScannerConnection.disconnectMediaScanner();
}
// look for all rows with deleted flag set and delete the rows from the database
// permanently
for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted) {
// this row is to be deleted from the database. but does it have
// mediaProviderUri?
if (TextUtils.isEmpty(info.mMediaProviderUri)) {
if (info.shouldScanFile()) {
// initiate rescan of the file to - which will populate
// mediaProviderUri column in this row
if (!scanFile(info, false, true)) {
throw new IllegalStateException("scanFile failed!");
}
continue;
}
} else {
// yes it has mediaProviderUri column already filled in.
// delete it from MediaProvider database.
getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null,
null);
}
// delete the file
deleteFileIfExists(info.mFileName);
// delete from the downloads db
getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
Downloads.Impl._ID + " = ? ",
new String[]{String.valueOf(info.mId)});
}
}
}
}
其实看着大部分代码 ,我们可能看不出来什么也不明白什么,我们只要看懂他的运行逻辑,每一步该做什么,不必在意每一个细节的问题,当我们遇到同样的事情的时候,可以只需要替换他某一步骤成我们的方法就可以进行自己的修改了。好接着看run方法,我们主要看代码的48-53行,在这里获得了一个downloadInfo对象(根据id),当downloadInfo不为null的时候执行updateDownload,
private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
int oldVisibility = info.mVisibility;
int oldStatus = info.mStatus;
reader.updateFromDatabase(info);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "processing updated download " + info.mId +
", status: " + info.mStatus);
}
boolean lostVisibility =
oldVisibility == Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& info.mVisibility != Downloads.Impl.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& Downloads.Impl.isStatusCompleted(info.mStatus);
boolean justCompleted =
!Downloads.Impl.isStatusCompleted(oldStatus)
&& Downloads.Impl.isStatusCompleted(info.mStatus);
if (lostVisibility || justCompleted) {
mSystemFacade.cancelNotification(info.mId);
}
info.startIfReady(now, mStorageManager);
}
updateDownload方法里面其实就是根据数据库里面的数据,更新一些Notification的显示状态,最后继续执行info.startIfReady.
如果info为null我们就开始执行insertDownload
private DownloadInfo insertDownload(DownloadInfo.Reader reader, long now) {
DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade);
mDownloads.put(info.mId, info);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "processing inserted download " + info.mId);
}
info.startIfReady(now, mStorageManager);
return info;
}
其实就是创建一个新的DownloadInfo将其放入mDownloads的map集合里,最后和上面的updateDownload一样,执行info.startIfReady
void startIfReady(long now, StorageManager storageManager) {
if (!isReadyToStart(now)) {
return;
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
}
if (mStatus != Impl.STATUS_RUNNING) {
mStatus = Impl.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Impl.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
}
DownloadHandler.getInstance().enqueueDownload(this);
}
在startIfReady中,首先检查下载任务是否正在下载,接着更新数据库数据,最后调用DownloadHandler的enqueueDownload方法执行下载有关的操作
synchronized void enqueueDownload(DownloadInfo info) {
if (!mDownloadsQueue.containsKey(info.mId)) {
if (Constants.LOGV) {
Log.i(TAG, "enqueued download. id: " + info.mId + ", uri: " + info.mUri);
}
mDownloadsQueue.put(info.mId, info);
startDownloadThread();
}
}
查看DownloadHandler中的enqueueDownload最后其实就是开启了下载的线程startDownloadThrea
private synchronized void startDownloadThread() {
Iterator keys = mDownloadsQueue.keySet().iterator();
ArrayList ids = new ArrayList();
while (mDownloadsInProgress.size() < mMaxConcurrentDownloadsAllowed && keys.hasNext()) {
Long id = keys.next();
DownloadInfo info = mDownloadsQueue.get(id);
info.startDownloadThread();
ids.add(id);
mDownloadsInProgress.put(id, mDownloadsQueue.get(id));
if (Constants.LOGV) {
Log.i(TAG, "started download for : " + id);
}
}
for (Long id : ids) {
mDownloadsQueue.remove(id);
}
}
其实就是调用了info.startDownloadThread,并将我们的下载任务的id存储起来。
void startDownloadThread() {
DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this,
StorageManager.getInstance(mContext));
mSystemFacade.startThread(downloader);
}
新建DownloadThread,并启动起来。来看下载线程的run方法。
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
State state = new State(mInfo);
AndroidHttpClient client = null;
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
String errorMsg = null;
final NetworkPolicyManager netPolicy = NetworkPolicyManager.getSystemService(mContext);
final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
try {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.acquire();
// while performing download, register for rules updates
netPolicy.registerListener(mPolicyListener);
if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
}
client = AndroidHttpClient.newInstance(userAgent(), mContext);
// network traffic on this thread should be counted against the
// requesting uid, and is tagged with well-known value.
TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
TrafficStats.setThreadStatsUid(mInfo.mUid);
boolean finished = false;
while(!finished) {
Log.i(Constants.TAG, "Initiating request for download " + mInfo.mId);
// Set or unset proxy, which may have changed since last GET request.
// setDefaultProxy() supports null as proxy parameter.
ConnRouteParams.setDefaultProxy(client.getParams(),
Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
HttpGet request = new HttpGet(state.mRequestUri);
try {
executeDownload(state, client, request);
finished = true;
} catch (RetryDownload exc) {
// fall through
} finally {
request.abort();
request = null;
}
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
}
finalizeDestinationFile(state);
finalStatus = Downloads.Impl.STATUS_SUCCESS;
} catch (StopRequestException error) {
// remove the cause before printing, in case it contains PII
errorMsg = error.getMessage();
String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg);
if (Constants.LOGV) {
Log.w(Constants.TAG, msg, error);
}
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
errorMsg = ex.getMessage();
String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
Log.w(Constants.TAG, msg, ex);
finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
TrafficStats.clearThreadStatsTag();
TrafficStats.clearThreadStatsUid();
if (client != null) {
client.close();
client = null;
}
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
state.mGotData, state.mFilename,
state.mNewUri, state.mMimeType, errorMsg);
DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
netPolicy.unregisterListener(mPolicyListener);
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
}
mStorageManager.incrementNumDownloadsSoFar();
}
这里才是真正的访问网络的操作,具体操作我们就不一一细细解读了。到这里其实运用DownloadManager进行下载的逻辑基本通读了一遍。
其实还有一个细节的操作在我们最开始的downloadUtil的download方法中之后一句代码downLoadManager.enqueue(request);会返回一个long行的id,这个id就是我们provider中存放的id。我们可以将这个id记录在我们的文件或者sharedPreference中,当我们由于某种原因(网络或者认为原因)使下载中断的时候,我们可以再下次再次进入服务器的时候,继续请求服务器,根据这个id去查询数据库中的文件的下载完成状态是否完成,如果位完成,继续下载或者删除数据库和下载未完成的部分文件,重新下载,如果下载完成,直接在下载的文件夹找到相应的文件提示下载安装。
还有就是,当我们手机中的其他应用也用到DownloadManager进行他们的下载任务的时候,也会记录到数据库中,但是有这么一种可能,当我们2个应用同时下载更新,并且下载完成之后发出广播通知安装的时候,我们可以根据自己的sharedPreference中所记录的id找到自己app下载的文件,检查它的下载状态,最后决定是否执行安装或者其他 操作,这样管理文件的时候以至于不会混乱。
这是个人在做项目的时候的一些学习和使用DownloadManager的观点,记录下来方便自己下次使用,也供大家参考。