DownloadManager的使用和解析

DownloadManager的介紹

DownloadManger是android 2.3(API 9)開始提供的系統服務,用於處理長時間的下載操作。應用場景是客戶端請求一個URL地址去下載一個目標文件。DownloadManger可以構建一個後臺下載服務,在發生故障或連接更改、重新啓動系統等情況後,處理HTTP連接並重試下載。

如果APP通過DownloadManager請求下載,那麼應用註冊ACTION_NOTIFICATION_CLICKED的廣播,以便在用戶單擊下載通知欄或者下載UI時,進行適當處理。

需要注意使用DownloadManager時,必須申請Manifest.permission.INTERNET權限。

獲取這個類的實例的方式有:Context.getSystemService(Class),參數爲DownloadManager.class,或者,Context.getSystemService(String),其參數爲Context.DOWNLOAD_SERVICE

主要的接口和類:

1、內部類DownloadManager.Query,這個類可以用於過濾DownloadManager的請求。

2、內部類DownloadManager.Request,這個類包含請求一個新下載連接的必要信息。

3、公共方法enqueue,在隊列中插入一個新的下載。當連接正常
,並且DownloadManager準備執行這個請求時,開始自動下載。返回結果是系統提供的唯一下載ID,這個ID可以用於與這個下載相關的回調。

4、公共方法query,用於查詢下載信息。

5、公共方法remove,用於刪除下載,如果下載中則取消下載。同時會刪除下載文件和記錄。

DownloadManager的使用

1、在AndroidManifest中添加權限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

一個是網絡訪問權限,一個是SDCARD寫權限。

2、初始化DownloadManager.Request,調用enqueue方法開始下載

DownloadManager mDownloadManager = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);

String apkUrl = “https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk”;
Uri resource = Uri.parse(apkUrl);

Request request = new Request(resource);
//下載的本地路徑,表示設置下載地址爲SD卡的Download文件夾,文件名爲mobileqq_android.apk。
request.setDestinationInExternalPublicDir(“Download”, “mobileqq_android.apk”);

//start 一些非必要的設置
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE | Request.NETWORK_WIFI);
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setVisibleInDownloadsUi(true);
request.setTitle(displayName);
//end 一些非必要的設置

mDownloadManager.enqueue(request);

DownloadManager.Request除了構造函數的Uri必須外,其他設置都爲可選設置。例如:

request.setMimeType(“application/cn.trinea.download.file”);

設置下載文件的mineType。因爲在下載管理UI中,點擊某個已下載完成文件,以及,在下載完成後,點擊通知欄提示,都會根據mimeType去打開文件,所以我們可以利用這個屬性。比如設置了mimeType爲application/cn.trinea.download.file,我們可以同時設置某個Activity的intent-filter爲application/cn.trinea.download.file,用於響應點擊的打開文件。

<intent-filter>
	<action android:name="android.intent.action.VIEW" />
 
	<category android:name="android.intent.category.DEFAULT" />
 
	<data android:mimeType="application/cn.trinea.download.file" />
</intent-filter>

3、下載進度狀態的監聽及查詢

DownloadManager沒有提供相應的回調接口,用於返回實時的下載進度狀態,但通過四大組件之一ContentProvider,可以監聽到當前下載項的進度狀態變化。

DownloadManager.getUriForDownloadedFile(id);

該方法會返回一個下載項的Uri,如content://downloads/my_downloads/125,因此,我們通過ContentObserver監聽Uri.parse(“content://downloads/my_downloads”),觀察這個Uri指向的數據庫項的變化,然後進行下一步操作,如發送handler進行更新UI。例子如下:

private Handler handler = new Handler(Looper.getMainLooper());
private static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");
private DownloadContentObserver observer = new DownloadStatusObserver();

class DownloadContentObserver extends ContentObserver {
	public DownloadContentObserver() {
		super(handler);
	}

	@Override
	public void onChange(boolean selfChange) {
		updateView();
	}

}

@Override
protected void onResume() {
	super.onResume();
	getContentResolver().registerContentObserver(CONTENT_URI, true, observer);
}

@Override
protected void onDestroy() {
	super.onDestroy();
	getContentResolver().unregisterContentObserver(observer);
}

public void updateView() {
	int[] bytesAndStatus = getBytesAndStatus(downloadId);
	int currentSize = bytesAndStatus[0];//當前大小
	int totalSize = bytesAndStatus[1];//總大小
	int status = bytesAndStatus[2];//下載狀態
	Message.obtain(handler, 0, currentSize, totalSize, status).sendToTarget();
}

public int[] getBytesAndStatus(long downloadId) {
    int[] bytesAndStatus = new int[] { -1, -1, 0 };
    Query query = new Query().setFilterById(downloadId);
    Cursor c = null;
    try {
        c = mDownloadManager.query(query);
        if (c != null && c.moveToFirst()) {
            bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
            bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
            bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
        }
    } finally {
        if (c != null) {
            c.close();
        }
    }
    return bytesAndStatus;
}

上面的代碼主要調用queue()進行查詢,參數屬性封裝在DownloadManager.Query()類中。這個類主要包括以下接口:

  • setFilterById(long… ids),根據下載id進行過濾
  • setFilterByStatus(int flags),根據下載狀態進行過濾
  • setOnlyIncludeVisibleInDownloadsUi(boolean value),根據是否在Download UI中可見進行過濾。
  • orderBy(String column, int direction),根據列進行排序,不過目前僅支持
    DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP
    DownloadManager.COLUMN_TOTAL_SIZE_BYTES排序。

補充

如果界面上過多元素需要更新,且網速較快不斷的執行onChange會對頁面性能有一定影響,或者出現一些異常情況,那麼推薦ScheduledExecutorService定期查詢,如下:

//三秒定時刷新一次 
public static ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); 
Runnable command = new Runnable() { 
	@Override 
	public void run() { 
		updateView(); 
	} 
};
scheduledExecutorService.scheduleAtFixedRate(command, 0, 3, TimeUnit.SECONDS);

4、下載成功監聽

下載完成後,下載管理服務會發出DownloadManager.ACTION_DOWNLOAD_COMPLETE這個廣播,並傳遞downloadId作爲參數。通過接受廣播我們可以打開對下載完成的內容進行操作。

private CompleteReceiver completeReceiver;

class CompleteReceiver extends BroadcastReceiver {

	@Override
	public void onReceive(Context context, Intent intent) {
		// get complete download id
		long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
		// to do here
	}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	//...
	
	completeReceiver = new CompleteReceiver();
	//register download success broadcast
	registerReceiver(completeReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}

@Override
protected void onDestroy() {
	super.onDestroy();
	unregisterReceiver(completeReceiver);
}

5、響應通知欄的點擊

(1)下載中點擊

點擊下載中通知欄提示,系統會對下載的應用單獨發送Action爲DownloadManager.ACTION_NOTIFICATION_CLICKED廣播。intent.getData爲content://downloads/all_downloads/29669,最後一位爲downloadId。
如果同時下載多個應用,intent會包含DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS這個key,表示下載的downloadId數組。

(2)下載完成後點擊

下載完成後系統會調用下面代碼進行處理,從中我們可以發現系統會調用View Action根據mimeType去查詢。所以可以利用上文第2條介紹的DownloadManager.Request的setMimeType函數。

private void openDownload(Context context, Cursor cursor) {
	String filename = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl._DATA));
	String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Downloads.Impl.COLUMN_MIME_TYPE));
	Uri path = Uri.parse(filename);
	// If there is no scheme, then it must be a file
	if (path.getScheme() == null) {
		path = Uri.fromFile(new File(filename));
	}
	Intent activityIntent = new Intent(Intent.ACTION_VIEW);
	mimetype = DownloadDrmHelper.getOriginalMimeType(context, filename, mimetype);
	activityIntent.setDataAndType(path, mimetype);
	activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
	try {
		context.startActivity(activityIntent);
	} catch (ActivityNotFoundException ex) {
		Log.d(Constants.TAG, "no activity for " + mimetype, ex);
	}
}

DownloadManager的解析

DownloadManager開始下載的入口是enqueue方法,這個方法的源碼如下:

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;
}

使用的ContentProvider方式,將Request信息轉換爲ContentValues類,然後調用ContentResolver進行插入,底層會調用對應的ContentProvider的insert方法。URI是Downloads.Impl.CONTENT_URI,即"content://downloads/my_downloads",找到對應的Provider即系統提供的DownloadProvider

DownloadProvider類在系統源碼的src/com/android/providers/downloads的路徑下,找都其insert方法的實現,可以發現最後部分的代碼:

public Uri insert(final Uri uri, final ContentValues values) {
    ...
    // Always start service to handle notifications and/or scanning
    final Context context = getContext();
    context.startService(new Intent(context, DownloadService.class));

    return ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID);
}

即插入信息後,會啓動DownloadService開始進行下載。(從Android N (API 24) 開始實現方式不同

DownloadService的入口是onStartCommand方法,其中用mUpdateHandler發送消息MSG_UPDATE,mUpdateHandler處理消息的方式如下:

mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);

private Handler.Callback mUpdateCallback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        ...
        final boolean isActive;
        synchronized (mDownloads) {
            isActive = updateLocked();
        }
        ...
    }
};

private boolean updateLocked() {
	...
     // Kick off download task if ready
	 final boolean activeDownload = info.startDownloadIfReady(mExecutor);
    ...
}

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);
            }
			//啓動DownloadThread開始下載任務
            mTask = new DownloadThread(mContext, mSystemFacade, mNotifier, this);
            mSubmittedTask = executor.submit(mTask);
        }
        return isReady;
    }
}

從上面源碼可以看,DownloadService的onStartCommand方法,最終啓動DownloadThread,開始下載的任務(網絡請求接口使用的是HttpURLConnection)。DownloadThread在下載過程中,會更新DownloadProvider。

綜上所述,DownloadManager的enqueue方法的流程是:

DownloadProvider插入信息 >> 啓動DownloadService >> 開始DownloadThread進行下載

參考

https://developer.android.com/reference/android/app/DownloadManager

Android系統下載管理DownloadManager功能介紹及使用示例

Android系統下載管理DownloadManager

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章