由于身边的同事离职,最近又接手了一个模块,DownloadProvider, 也就是安卓中自带的下载管理。此模块的代码量比较少,但是最近阅读代码却发现还是由不少知识点的。之前同事在此模块做了一个关于DRM的需求,查看了一下其代码修改量也是比较大的,最近需要进行ROM移植,发现直接看此部分代码不知所云。
为了能够实现后期顺利移植改功能,首先需要对此模块的整体流程有个比较清晰的认识。于是在项目其他问题还比较少的最近两天,开始断断续续地调试代码,增加log,查看其运行流程。
首先看清单文件AndroidManifest.xml, 一堆权限声明在此,大概了解到了下载时有很多限制权限,比如说需要应用的签名和系统相同或者是系统应用
android:protectionLevel="signatureOrSystem"
也许也正是因为此,外部调用下载需要使用DownloadManager来进行,因此在刚开始时也从网络上查找了使用接口来进行下载的方法,基本就三点:
1、获取下载管理服务
DownloadManager manger=getSystemService(Context.DOWNLOAD_SERVICE) ;
2、构建下载请求对象
DownloadManager.Request down=new DownloadManager.Request (Url.parse(http://122.com/222.apk));
ps 这个http地址可以在网页上任意找一个下载文件,然后用审查元素,查看网页源码,获得下载URL地址。
3、加入下载队列
manger.enqueue(down);
由此三步即可最终调用downloadProvider模块来实现下载。
DownloadManager manger = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request down=new DownloadManager.Request (Uri.parse("http://www.romjd.com/Rom/Download/74058/0?from=onekey"));
manger.enqueue(down);
Toast.makeText(this," begin to download", Toast.LENGTH_LONG).show();
此部分代码是放在拨号盘中进行调试,然后编译的,单独编译的apk需要和系统相同的签名才可以正常下载,这个没有尝试, 但是将downloadProvider中的权限都改为normal,以及注释掉插入数据库时的权限检查后都没有能成功下载,还是会报权限问题。
------------------------------随意的分割线-----------------------
下面来分析一下下载管理流程,其大体实现是外部调用下载会先往数据库中插入许多跟下载相关的字段信息,然后启动service来使用消息机制,查询数据库中的内容,最后逐个启动线程进行下载, 下载的过程中会往状态栏发送通知,并更新下载进度,当所有的下载完成之后,service会自动关闭。
1、插入数据库
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values); ---权限检查
long rowID = db.insert(DB_TABLE, null, filteredValues);--插入数据库 downloads 表
insertRequestHeaders(db, rowID, values);---插入头信息到数据库 request_headers表
notifyContentChanged(uri, match);---通知数据库变化,刚开始感觉此处没什么卵用,因为监听还没注册,这个是在服务中进行的,后来才知道开机启动后,服务就已经创建了。
context.startService(new Intent(context, DownloadService.class)); ---启动服务
return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}
2、下载服务DownloadService运行
public void onCreate() {
super.onCreate();
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(this);---这个目前不知道具体用途,在通知栏的点击响应中有调用此类的方法发送广播
}
mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
mUpdateThread.start();
mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);---服务中用来进行消息处理的handler,这个对调很关键。
Log.v(Constants.TAG, "Service onCreate mUpdateHandler ");
mScanner = new DownloadScanner(this);---跟文件扫描相关的,没怎么关注
mNotifier = new DownloadNotifier(this);---通知栏相关
mNotifier.cancelAll();
mObserver = new DownloadManagerContentObserver();
getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
true, mObserver);---注册下载管理中数据库变化监听的观察着
}
public int onStartCommand(Intent intent, int flags, int startId) {
int returnValue = super.onStartCommand(intent, flags, startId);
mLastStartId = startId;
enqueueUpdate();---更新队列,其实就是发了个消息,然后也没有具体正对这个的处理,但是会运行handleMessage中的代码
return returnValue;
}
private Handler.Callback mUpdateCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
final int startId = msg.arg1;
final boolean isActive;
synchronized (mDownloads) {
isActive = updateLocked();---加锁进行更新数据库,查询之前插入到数据库的内容,根据查询结果决定是否要启动下载
}
if (msg.what == MSG_FINAL_UPDATE) {
// Dump thread stacks belonging to pool
for (Map.Entry<Thread, StackTraceElement[]> entry :
Thread.getAllStackTraces().entrySet()) {
if (entry.getKey().getName().startsWith("pool")) {
Log.d(TAG, entry.getKey() + ": " + Arrays.toString(entry.getValue()));
}
}
// Dump speed and update details
mNotifier.dumpSpeeds();
Log.wtf(TAG, "Final update pass triggered, isActive=" + isActive
+ "; someone didn't update correctly.");
}
if (isActive) {
// Still doing useful work, keep service alive. These active
// tasks will trigger another update pass when they're finished.
// Enqueue delayed update pass to catch finished operations that
// didn't trigger an update pass; these are bugs.
enqueueFinalUpdate();
} else {
// No active tasks, and any pending update messages can be
// ignored, since any updates important enough to initiate tasks
// will always be delivered with a new startId.
if (stopSelfResult(startId)) {
if (DEBUG_LIFECYCLE) Log.v(TAG, "Nothing left; stopped");
getContentResolver().unregisterContentObserver(mObserver);
mScanner.shutdown();
mUpdateThread.quit();
}
}
return true;
}
};
private boolean updateLocked() {
final long now = mSystemFacade.currentTimeMillis();
boolean isActive = false;
long nextActionMillis = Long.MAX_VALUE;
final Set<Long> staleIds = Sets.newHashSet(mDownloads.keySet());
final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
null, null, null, null);
try {
final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID);
while (cursor.moveToNext()) {
final long id = cursor.getLong(idColumn);
staleIds.remove(id);
if (info.mDeleted) {---如果标记为删除的,则删除文件和数据库记录
// Delete download if requested, but only after cleaning up
if (!TextUtils.isEmpty(info.mMediaProviderUri)) {
resolver.delete(Uri.parse(info.mMediaProviderUri), null, null);
}
deleteFileIfExists(info.mFileName);
resolver.delete(info.getAllDownloadsUri(), null, null);
} else {---否则启动下载
// Kick off download task if ready
final boolean activeDownload = info.startDownloadIfReady(mExecutor);
// Kick off media scan if completed
final boolean activeScan = info.startScanIfReady(mScanner);
isActive |= activeDownload;
isActive |= activeScan;
}
// Keep track of nearest next action
nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
}
} finally {
cursor.close();
}
// Clean up stale downloads that disappeared
for (Long id : staleIds) {
deleteDownloadLocked(id);
}
// Update notifications visible to user
mNotifier.updateWith(mDownloads.values());---更新到通知栏
return isActive;
}
public boolean startDownloadIfReady(ExecutorService executor) {
synchronized (this) {
final boolean isReady = isReadyToDownload();---根据状态确定是否可以进行下载
final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
if (isReady && !isActive) {
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);
}
mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
Log.v(TAG, "startDownloadIfReady new DownloadThread");
mSubmittedTask = executor.submit(mTask);
}
return isReady;
}
}
3、下载线程DownloadThread
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
try {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
wakeLock.setWorkSource(new WorkSource(mInfo.mUid));
wakeLock.acquire();
executeDownload();---执行下载
}
}
private void executeDownload() throws StopRequestException {
switch (responseCode) {
case HTTP_OK:
if (resuming) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
parseOkHeaders(conn);---解析头信息,主要是通过各种形式获取文件名字,其中最后如果实在无法找到合适的名字,会用随机算法生成,在命好名字之后,会以此新建一个空文件。
transferData(conn);---这个才是正在填充文件数据的地方,里面会计算当前空间是否足以容纳此文件,当然里面也包含了计算当前下载进度的内容。
return;
}
4、通知栏处理DownloadNotifier
这个类中是用来处理下载过程中在通知栏显示进度的,其中看了一下好久没有用的Notification类,以及实现点击跳转的PendingIntent, 当看到此处时,突然想起了当年刚开始搞安卓,在The Creative Life 公司做短信模块,经常要用到这个,时隔一年多,现在又遇到了,像是看到了老朋友,由于久未交谈,有点陌生,于是又查询了谷歌的SDK文档看了下,渐渐又熟悉起来。
PendingIntent.getBroadcast最终在DownloadReceiver类中得到响应处理点击通知栏中的下载进入事宜。
此处当时调试时还出现了一个让我纠结许久的小玩意, 都知道点击通知栏的下载进度之后,如果有多个下载任务,会跳转到下载管理界面,但是我在阅读代码的时候,看到代码的处理最后只是发送了一个广播:
mSystemFacade.sendBroadcast(appIntent);
但是始终没有找到广播响应的地方,于是反过来搜索看哪里有标明调用界面类的地方,也没有得到结果,于是还怀疑最终跳转到界面的代码是不是这个广播起的作用,将其屏蔽之后,果然跳转不了了。
思来想去只有在全部的安卓源码中进行搜索,最后在浏览器中发现了处理该广播发送Intent的代码,这才明白是在浏览器中进行的处理。再回头查看androidManifest中的界面文件接收的Intent果然如此:
<activity android:name="com.android.providers.downloads.ui.DownloadList"
<intent-filter>
<action android:name="android.intent.action.VIEW_DOWNLOADS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
有时候山重水复疑无路,但是换个思维想想就会柳暗花明又一村。