android系統(tǒng)瀏覽器下載流程

標(biāo)簽: android browser download


簡(jiǎn)介

當(dāng)我們用瀏覽器點(diǎn)開一個(gè)下載鏈接检激,然后去下載齐莲,從宏觀上認(rèn)識(shí)嚣鄙,有下載進(jìn)度的實(shí)時(shí)更新和界面的跳轉(zhuǎn)放祟。整個(gè)過程中鳍怨,主要涉及到以下過程。瀏覽器點(diǎn)擊下載按鈕跪妥,瀏覽器分發(fā)下去一個(gè)下載請(qǐng)求鞋喇,跳轉(zhuǎn)界面的同時(shí)在DownloadProvider進(jìn)程中去真正的下載數(shù)據(jù)以及更新數(shù)據(jù)庫,在界面上監(jiān)聽數(shù)據(jù)庫的變化眉撵,去實(shí)時(shí)更新相關(guān)進(jìn)度侦香。全過程中,Browser進(jìn)程負(fù)責(zé)分發(fā)下載請(qǐng)求纽疟,DownloadProvider進(jìn)程負(fù)責(zé)真正的下載操作罐韩。

目前而言,主要有兩種結(jié)構(gòu)污朽,C-S和B-S結(jié)構(gòu)散吵。對(duì)于Browser來說,主要在于對(duì)Webview這個(gè)控件的認(rèn)識(shí)蟆肆,底層的內(nèi)核實(shí)現(xiàn)也是非常復(fù)雜矾睦,這里我們不做討論。對(duì)于一個(gè)瀏覽器鏈接颓芭,webkit底層會(huì)去解析顷锰,同時(shí)也會(huì)判斷這個(gè)鏈接屬于什么類型。比如我們今天的這個(gè)下載鏈接亡问,Browser就有專門的下載監(jiān)聽器去回調(diào)執(zhí)行這個(gè)action官紫,下面我們會(huì)詳細(xì)分析肛宋。

WebView控件簡(jiǎn)單介紹

WebView控件提供了一個(gè)內(nèi)嵌的瀏覽器試圖,用于顯示本地的html或網(wǎng)路上的網(wǎng)頁束世。
并且比較強(qiáng)大的是酝陈,還可以直接跟js相互調(diào)用。
WebView有兩個(gè)方法:setWebChromeClient和setWebClient
WebChromeClient:主要處理解析毁涉,渲染網(wǎng)頁等瀏覽器做的事情沉帮,也是輔助WebView處理Javascript 的對(duì)話框,網(wǎng)站圖標(biāo)贫堰,網(wǎng)站title穆壕,加載進(jìn)度等
WebViewClient :就是幫助WebView處理各種通知、請(qǐng)求事件的其屏。

Browser下載的時(shí)序圖喇勋。

這里寫圖片描述

下面來詳細(xì)分析具體的代碼實(shí)現(xiàn)細(xì)節(jié),時(shí)序圖是更加細(xì)節(jié)的步驟偎行,這里我們著重分析下面的流程川背。


Step 1:Tab.setWebView

      void setWebView(WebView w, boolean restore) {
        ....
        mMainView = w;
        // attach the WebViewClient, WebChromeClient and DownloadListener
        if (mMainView != null) {
            mMainView.setWebViewClient(mWebViewClient);
            mMainView.setWebChromeClient(mWebChromeClient);
            mMainView.setDownloadListener(mDownloadListener);
            ....
        }
    }

這個(gè)方法定義在packages/apps/Browser/src/com/android/browser/Tab.java
瀏覽器是用過Webview來顯示UI。這里設(shè)置了一個(gè)WebView對(duì)象蛤袒,然后setWebViewClient和setWebChromeClient主要設(shè)置了對(duì)頁面加載以及js的處理熄云。這里我們只分析setDownloadListener這個(gè)監(jiān)聽,首先要理解一點(diǎn)妙真,對(duì)于WebView上的一個(gè)下載按鈕缴允,它的事件是怎么處理的,瀏覽器如何判斷這個(gè)是下載隐孽?以上其實(shí)瀏覽器內(nèi)核已經(jīng)處理癌椿,瀏覽器內(nèi)核是根據(jù)指定的url判斷該鏈接是否是一個(gè)下載鏈接健蕊,如果點(diǎn)擊的是一個(gè)下載鏈接菱阵,那么最終會(huì)回調(diào)到該監(jiān)聽器中去處理,具體底層實(shí)現(xiàn)比較復(fù)雜缩功,暫不作討論晴及。

 Tab(WebViewController wvcontroller, WebView w, Bundle state) {
        /// M: add for save page
        ....
        mDownloadListener = new BrowserDownloadListener() {
            public void onDownloadStart(String url, String userAgent,
                    String contentDisposition, String mimetype, String referer,
                    long contentLength) {
                /// M: add for fix download page url
                mCurrentState.mIsDownload = true;
                mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition,
                        mimetype, referer, contentLength);
            }
        };
        ....
        setWebView(w);
        ....
    }

這個(gè)方法定義在packages/apps/Browser/src/com/android/browser/Tab.java
分析Tab的構(gòu)造方法,這里主要看BrowserDownloadListener這個(gè)對(duì)象嫡锌。當(dāng)點(diǎn)擊了下載按鈕虑稼,則會(huì)去回調(diào)BrowserDownloadListener的onDownloadStart方法,這個(gè)最終是委托給了mWebViewController去處理势木。

Step 2:WebViewController.onDownloadStart

    @Override
    public void onDownloadStart(Tab tab, String url, String userAgent,
            String contentDisposition, String mimetype, String referer,
            long contentLength) {
        ....
        DownloadHandler.onDownloadStart(mActivity, url, userAgent,
                contentDisposition, mimetype, referer, false, contentLength);
        ...
    }

這個(gè)方法定義在packages/apps/Browser/src/com/android/browser/Controller.java
WebViewController是一個(gè)接口蛛倦,Controller是它的具體實(shí)現(xiàn),在onDownloadStart方法中啦桌,實(shí)現(xiàn)比較簡(jiǎn)單溯壶,直接是將參數(shù)委托給DownloadHandler的靜態(tài)方法onDownloadStart去進(jìn)一步處理及皂。
在這里,參數(shù):
url下載的網(wǎng)址鏈接
userAgent瀏覽器userAgent信息
mimetype下載內(nèi)容的type類型
contentLength下載內(nèi)容大小

Step 3:DownloadHandler.onDownloadStart

這個(gè)方法定義在packages/apps/Browser/src/com/android/browser/DownloadHandler.java
實(shí)現(xiàn)很簡(jiǎn)單且改,直接將參數(shù)繼續(xù)傳遞到onDownloadStartNoStream方法验烧。

Step 4:DownloadHandler.onDownloadStartNoStream

    /*package */
    public static void onDownloadStartNoStream(Activity activity,
            String url, String userAgent, String contentDisposition,
            String mimetype, String referer, boolean privateBrowsing, long contentLength) {
        ....
        // java.net.URI is a lot stricter than KURL so we have to encode some
        // extra characters. Fix for b 2538060 and b 1634719
        WebAddress webAddress;
        try {
            webAddress = new WebAddress(url);
            webAddress.setPath(encodePath(webAddress.getPath()));
        } catch (Exception e) {
            // This only happens for very bad urls, we want to chatch the
            // exception here
            Log.e(LOGTAG, "Exception trying to parse url:" + url);
            return;
        }
    
        String addressString = webAddress.toString();
        Uri uri = Uri.parse(addressString);
        final DownloadManager.Request request = new DownloadManager.Request(uri);
        request.setMimeType(mimetype);

        // let this downloaded file be scanned by MediaScanner - so that it can
        // show up in Gallery app, for example.
        request.allowScanningByMediaScanner();
        request.setDescription(webAddress.getHost());
        // XXX: Have to use the old url since the cookies were stored using the
        // old percent-encoded url.
        String cookies = CookieManager.getInstance().getCookie(url, privateBrowsing);
        request.addRequestHeader("cookie", cookies);
        request.addRequestHeader("User-Agent", userAgent);
        request.addRequestHeader("Referer", referer);
        request.setNotificationVisibility(
                DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setUserAgent(userAgent);
      
        final DownloadManager manager = (DownloadManager)   
                        activity.getSystemService(Context.DOWNLOAD_SERVICE);
        new Thread("Browser download") {
                public void run() {
                    manager.enqueue(request);
                }
        }.start();
        /// M: Add to start Download activity. @{
        Intent pageView = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
        pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        activity.startActivity(pageView);
        /// @}
    }

這個(gè)方法定義在packages/apps/Browser/src/com/android/browser/DownloadHandler.java
在該方法中,主要做了三件事
1.將下載信息url,minetype等封裝成一個(gè)Request對(duì)象又跛,供后續(xù)使用碍拆。
2.獲取一個(gè)DownloadManager對(duì)象,將前面封裝的Request對(duì)象慨蓝,安排到下載隊(duì)列
3.開始下載的同時(shí)感混,去跳轉(zhuǎn)UI界面,同步顯示UI信息礼烈。
這里我們重點(diǎn)分析數(shù)據(jù)流程這塊浩习,接下來分析enqueue這個(gè)方法的具體實(shí)現(xiàn)。

Step 5:DownloadManager.enqueue

    public long enqueue(Request request) {
        ContentValues values = request.toContentValues(mPackageName);
        Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
        if (downloadUri != null) {
            long id = Long.parseLong(downloadUri.getLastPathSegment());
            return id;
        }
        return -1;
    }

這個(gè)方法定義在frameworks/base/core/java/android/app/DownloadManager.java
首先toContentValues將Request的信息要存數(shù)據(jù)庫的字段轉(zhuǎn)化為一個(gè)ContentValues對(duì)象济丘,以上幾步都是在Browser進(jìn)程中進(jìn)行的谱秽,接下來insert方法,通過uri開始最終跨進(jìn)程請(qǐng)求去插入數(shù)據(jù)摹迷。這里Downloads.Impl.CONTENT_URI為content://downloads/my_downloads疟赊,從pacakges/providers/DownloadProvider的清單文件中很容易知道最終是調(diào)用了DownloadProvider的insert方法去插入數(shù)據(jù)。
pacakges/providers/DownloadProvider的清單文件如下:

  ....
  <provider android:name=".DownloadProvider"
                  android:authorities="downloads" android:exported="true">
  ....

Step 6:DownloadProvider.insert

    @Override
    public Uri insert(final Uri uri, final ContentValues values) {
        ....
       long rowID = db.insert(DB_TABLE, null, filteredValues);
        if (rowID == -1) {
            Log.d(Constants.TAG, "couldn't insert into downloads database");
            return null;
        }
        insertRequestHeaders(db, rowID, values);
        notifyContentChanged(uri, match);
        // 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);
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadProvider.java
insert方法即是往DB_TABLE(downloads)表中插入了一條數(shù)據(jù)峡碉。接下來在insert方法最后啟動(dòng)DownloadService近哟,這幾步都是在DownloadProvider進(jìn)程中進(jìn)行的。接下來會(huì)有兩條主線鲫寄。
1吉执,在DownloadProvider進(jìn)程中啟動(dòng)的這個(gè)DownloadService繼續(xù)執(zhí)行。
2地来,返回到Step 4 Browser進(jìn)程的中的DownloadHandler.onDownloadStartNoStream方法中去跳轉(zhuǎn)界面戳玫。
這里我們不討論UI界面,接下來分析DownloadService的操作未斑。

Step 7:DownloadService.onCreate

   @Override
    public void onCreate() {
        super.onCreate();
        ....
        mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
        mUpdateThread.start();
        mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);

        mScanner = new DownloadScanner(this);
        mNotifier = new DownloadNotifier(this);
        mNotifier.cancelAll();

        mObserver = new DownloadManagerContentObserver();
        getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI,
                true, mObserver);
        ....
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadService.java
第一次啟動(dòng)咕宿,首次執(zhí)行onCreate方法,創(chuàng)建一個(gè)HandlerThread工作線程蜡秽,并注冊(cè)了一個(gè)監(jiān)聽數(shù)據(jù)庫改變的一個(gè)DownloadManagerContentObserver對(duì)象府阀,監(jiān)聽的uri為"content://downloads/all_downloads",第2個(gè)參數(shù)為true芽突,表示可以同時(shí)匹配其派生的Uri试浙。接下來進(jìn)入onStartCommand方法,在onStartCommand方法中繼續(xù)執(zhí)行enqueueUpdate方法寞蚌。

    public void enqueueUpdate() {
        if (mUpdateHandler != null) {
            mUpdateHandler.removeMessages(MSG_UPDATE);
            mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
        }
    }

這個(gè)方法執(zhí)行很簡(jiǎn)單田巴,首先是移除掉之前所有的MSG_UPDATE消息力细,然后再重新發(fā)送一個(gè)MSG_UPDATE消息,接下來分析Handler這個(gè)消息的回調(diào)實(shí)現(xiàn)固额。

  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) {
                mNotifier.dumpSpeeds();
            }

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

這個(gè)方法處理的邏輯比較多眠蚂,先整體上認(rèn)識(shí)這個(gè),主要有updateLocked方法主要負(fù)責(zé)具體的下載實(shí)現(xiàn)斗躏,它的返回值是一個(gè)boolean類型逝慧,用以判斷當(dāng)前下載是否是激活狀態(tài),也就是是否有下載任務(wù)啄糙。接下來如果判斷isActive為true笛臣,則會(huì)去執(zhí)行enqueueFinalUpdate方法。

  private void enqueueFinalUpdate() {
        mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
        mUpdateHandler.sendMessageDelayed(
                mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
                5 * MINUTE_IN_MILLIS);
    }

從這里我們可以看出隧饼,這個(gè)回調(diào)其實(shí)是當(dāng)有下載任務(wù)的時(shí)候沈堡,會(huì)一直的循環(huán)執(zhí)行下去,用以保證下載的任務(wù)的連續(xù)性燕雁,如果有中斷诞丽,則會(huì)重新啟動(dòng)。
下面我們來分析updateLocked的具體實(shí)現(xiàn)拐格,是如何將下載任務(wù)放入線程中去執(zhí)行的僧免,又是怎么知道有哪些下載任務(wù)的。

Step 8 :DownloadService.updateLocked

   private boolean updateLocked() {
        ...
        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);
                DownloadInfo info = mDownloads.get(id);
                if (info != null) {
                    updateDownload(reader, info, now);
                } else {
                    info = insertDownloadLocked(reader, now);
                }
                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);
        }
        ...
        return isActive;
    }

這個(gè)方法的實(shí)現(xiàn)分為幾步:
1.查詢downloads表中的所有記錄捏浊,接著將其封裝成一個(gè)DownloadInfo對(duì)象懂衩。
2.顯然第一次DownloadInfo的info是空值,接下來insertDownloadLocked會(huì)根據(jù)Cursor去新建一個(gè)DownloadInfo信息金踪。
3.DownloadInfo緩存的管理浊洞,將DownloadInfo緩存至mDownloads中管理。這里有個(gè)小的判斷分支胡岔,如果info.mDeleted為true法希,則刪除掉這條下載記錄,并且對(duì)應(yīng)的文件也將被刪除姐军,其實(shí)屬于邏輯控制铁材,跟下載無太大關(guān)系,不用太糾結(jié)奕锌。
4.對(duì)于一個(gè)新的下載,info.mDeleted顯然是false村生,所以會(huì)進(jìn)入到到else語句惊暴,調(diào)用DownloadInfo的startDownloadIfReady方法開始下載。
我們先分析insertDownloadLocked新建一個(gè)下載任務(wù)DownloadInfo的流程

    private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
        final DownloadInfo info = reader.newDownloadInfo(this, mSystemFacade, mNotifier);
        mDownloads.put(info.mId, info);

        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "processing inserted download " + info.mId);
        }

        return info;
    }

這個(gè)方法中趁桃,調(diào)用DownloadInfo.Reader去新建一個(gè)下載任務(wù)辽话,從前面可以看出肄鸽,這個(gè)reader對(duì)象是由數(shù)據(jù)庫Cursor進(jìn)行封裝的,具體分析reader.newDownloadInfo方法

       public DownloadInfo newDownloadInfo(
                Context context, SystemFacade systemFacade, DownloadNotifier notifier) 
            final DownloadInfo info = new DownloadInfo(context, systemFacade, notifier);
            updateFromDatabase(info);
            readRequestHeaders(info);
            return info;
        }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadInfo.java
Reader是DownloadInfo的一個(gè)靜態(tài)內(nèi)部類油啤,這個(gè)方法中典徘,首先是new了一個(gè)DownloadInfo對(duì)象,然后調(diào)用updateFromDatabase去更新DownloadInfo的一些屬性值益咬。實(shí)現(xiàn)比較簡(jiǎn)單逮诲,就是根據(jù)前面的Cursor對(duì)象,獲取數(shù)據(jù)庫的一些字段值保存在DownloadInfo中幽告。

從這里我們可以看出梅鹦,數(shù)據(jù)庫中所有的信息都會(huì)封裝成一個(gè)下載DownloadInfo,那么它是通過什么來判斷當(dāng)前數(shù)據(jù)是否是需要下載的任務(wù)呢冗锁?顯然如果這個(gè)url對(duì)應(yīng)的任務(wù)已經(jīng)被下載完成了齐唆,那么肯定是不需要再次下載的。接下來我們繼續(xù)往下走冻河,進(jìn)入到startDownloadIfReady這個(gè)方法箍邮。

Step 9:DownloadInfo.startDownloadIfReady

public boolean .startDownloadIfReady(ExecutorService executor) {
        synchroized (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);
                mSubmittedTask = executor.submit(mTask);
            }
            return isReady;
        }
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadInfo.java
我們先分析isReadyToDownload這個(gè)方法。

   private boolean isReadyToDownload() {
        ....
        switch (mStatus) {
            case 0: // status hasn't been initialized yet, this is a new download
            case Downloads.Impl.STATUS_PENDING: // download is explicit marked as ready to start
            case Downloads.Impl.STATUS_RUNNING: // download interrupted (process killed etc) while
                                                // running, without a chance to update the database
                return true;

            case Downloads.Impl.STATUS_WAITING_FOR_NETWORK:
            case Downloads.Impl.STATUS_QUEUED_FOR_WIFI:
                return checkCanUseNetwork(mTotalBytes) == NetworkState.OK;

            case Downloads.Impl.STATUS_WAITING_TO_RETRY:
                // download was waiting for a delayed restart
                final long now = mSystemFacade.currentTimeMillis();
                return restartTime(now) <= now;
            case Downloads.Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
                // is the media mounted?
                return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
            /// M: Because OMA DL spec, if insufficient memory, we
            /// will show to user but not retry.
            //case Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
                // should check space to make sure it is worth retrying the download.
                // but thats the first thing done by the thread when it retries to download
                // it will fail pretty quickly if there is no space.
                // so, it is not that bad to skip checking space availability here.
                //return true;
            /// M: Add for fix alp00406729, file already exist but user do not operation. @{
            case Downloads.Impl.STATUS_FILE_ALREADY_EXISTS_ERROR:
                return false;
            /// @}
        }
        return false;
    }

一切都明白了叨叙,這里就是根據(jù)mStatus這個(gè)字段媒殉,來判斷這個(gè)任務(wù)是否需要下載,也解決了我們之前的疑問摔敛,返回值為true的才會(huì)去執(zhí)行下載廷蓉,我們可以回頭看看Browser里面當(dāng)時(shí)insert一條下載記錄的時(shí)候,是沒有插入mStatus這個(gè)字段的马昙,所以對(duì)于一個(gè)新任務(wù)這里mStatus為默認(rèn)值即0桃犬,整個(gè)返回值為true。
接下來分析isActive這個(gè)boolean值行楞,它主要用來標(biāo)識(shí)當(dāng)前DownloadInfo是否在線程中去執(zhí)行了攒暇,保證一個(gè)DownloadInfo只執(zhí)行一次,對(duì)于新任務(wù)子房,顯然初始化的時(shí)候mSubmittedTask為null形用。

接下來進(jìn)入if語句,先update數(shù)據(jù)庫中的COLUMN_STATUS字段置為STATUS_RUNNING证杭。然后新建一個(gè)DownloadThread田度,放入到ExecutorService線程池中去執(zhí)行,這樣一個(gè)下載鏈接就正式開始下載了解愤。接下來分析下載讀寫文件以及更新數(shù)據(jù)庫的動(dòng)作镇饺。

Step 10:new DownloadThread

  @Override
    public void run() {
        ....
        if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId)
                == Downloads.Impl.STATUS_SUCCESS) {
            logDebug("Already finished; skipping");
            return;
        }
        ....
        executeDownload();
        ....
    }

這個(gè)方法定義在
DownloadThread是一個(gè)Runnable對(duì)象,這里我們關(guān)注構(gòu)造方法中的第4個(gè)參數(shù)送讲,即DownloadInfo奸笤,將DownloadInfo這個(gè)對(duì)象的信息惋啃,傳給DownloadThread的成員變量,還有DownloadInfoDelta對(duì)象监右,最后用于更新下載進(jìn)度數(shù)據(jù)庫信息边灭,我們后續(xù)分析。這樣就完全得到了這條下載信息的內(nèi)容健盒。接下來去執(zhí)行DownloadThread的run方法绒瘦,在新的線程中進(jìn)行下載。在run方法的實(shí)現(xiàn)中味榛,首先是再次確認(rèn)這個(gè)任務(wù)是需要下載的椭坚,否則直接return,線程結(jié)束搏色,然后如果需要下載則去調(diào)用executeDownload方法去執(zhí)行善茎。

  private void executeDownload() throws StopRequestException {
        .....
        URL url;
        try {
            // TODO: migrate URL sanity checking into client side of API
            url = new URL(mInfoDelta.mUri);
        } catch (MalformedURLException e) {
            throw new StopRequestException(STATUS_BAD_REQUEST, e);
        }

        int redirectionCount = 0;
        while (redirectionCount++ < Constants.MAX_REDIRECTS) {
            // Open connection and follow any redirects until we have a useful
            // response with body.
            HttpURLConnection conn = null;
            try {
                checkConnectivity();
                conn = (HttpURLConnection) url.openConnection();
                conn.setInstanceFollowRedirects(false);
                conn.setConnectTimeout(DEFAULT_TIMEOUT);
                conn.setReadTimeout(DEFAULT_TIMEOUT);

                addRequestHeaders(conn, resuming);

                final int responseCode = conn.getResponseCode();
                switch (responseCode) {
                    case HTTP_OK:
                        ....
                        /// @}
                        transferData(conn);
                        return;

             .....
        }

        throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
    }

在executeDownload方法中根據(jù)url創(chuàng)建一個(gè)HttpURLConnection連接。然后判斷getResponseCode網(wǎng)絡(luò)端返回值频轿。這里我們分析HTTP_OK的情況垂涯。在HTTP_OK:接下來調(diào)用transferData(conn);傳入的參數(shù)為這個(gè)HttpURLConnection這個(gè)連接。

Step 11:DownloadThread.transferData

   private void transferData(HttpURLConnection conn) throws StopRequestException {
        ....
        DrmManagerClient drmClient = null;
        ParcelFileDescriptor outPfd = null;
        FileDescriptor outFd = null;
        InputStream in = null;
        OutputStream out = null;
        try {
            try {
                in = conn.getInputStream();
            } catch (IOException e) {
                throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
            }
            try {
                outPfd = mContext.getContentResolver()
                        .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
                outFd = outPfd.getFileDescriptor();

                if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
                    drmClient = new DrmManagerClient(mContext);
                    out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
                } else {
                    out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
                }

                // Pre-flight disk space requirements, when known
                if (mInfoDelta.mTotalBytes > 0) {
                    final long curSize = Os.fstat(outFd).st_size;
                    final long newBytes = mInfoDelta.mTotalBytes - curSize;
                    StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
                    // We found enough space, so claim it for ourselves
                    Os.posix_fallocate(outFd, 0, mInfoDelta.mTotalBytes);
                }
                // Move into place to begin writing
                Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);

            } catch (ErrnoException e) {
                throw new StopRequestException(STATUS_FILE_ERROR, e);
            } catch (IOException e) {
                throw new StopRequestException(STATUS_FILE_ERROR, e);
            }
            // Start streaming data, periodically watch for pause/cancel
            // commands and checking disk space as needed.
            transferData(in, out, outFd);
            ....
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadThread.java
在這個(gè)方法中航邢,獲取一個(gè)該url對(duì)應(yīng)的網(wǎng)絡(luò)輸入流對(duì)象InputStream耕赘,同時(shí)根據(jù)uri構(gòu)造一個(gè)文件描述符,進(jìn)而構(gòu)建一個(gè)輸出流OutputStream對(duì)象膳殷,最后到重載的transferData方法操骡,將輸入輸出流,以及文件描述符傳入transferData開始存儲(chǔ)文件赚窃。

  private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
            throws StopRequestException {
        final byte buffer[] = new byte[Constants.BUFFER_SIZE];
        while (true) {
            checkPausedOrCanceled();
            int len = -1;
            try {
                len = in.read(buffer);
            } catch (IOException e) {
                throw new StopRequestException(
                        STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
            }
            if (len == -1) {
                break;
            }
            try {
                // When streaming, ensure space before each write
                if (mInfoDelta.mTotalBytes == -1) {
                    final long curSize = Os.fstat(outFd).st_size;
                    final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
                    StorageUtils.ensureAvailableSpace(mContext, outFd, newBytes);
                }
                out.write(buffer, 0, len);
                mMadeProgress = true;
                mInfoDelta.mCurrentBytes += len;
                updateProgress(outFd);
            } catch (ErrnoException e) {
                throw new StopRequestException(STATUS_FILE_ERROR, e);
            } catch (IOException e) {
                throw new StopRequestException(STATUS_FILE_ERROR, e);
            }
        }
       .....
    }

真正開始下載都是在這段code中册招,首先checkPausedOrCanceled方法檢查是否有取消下載請(qǐng)求,如果有直接進(jìn)入catch語句跳過勒极,下載結(jié)束是掰。如果沒有取消,則執(zhí)行while語句辱匿,執(zhí)行輸入輸出流的讀寫操作键痛。每一次讀寫的同時(shí)都會(huì)執(zhí)行updateProgress方法,顯然該方法是用來更新進(jìn)度的匾七,下面具體來分析絮短。

Step 12:DownloadThread.updateProgress

    private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
     ....
        final long bytesDelta = currentBytes - mLastUpdateBytes;
        final long timeDelta = now - mLastUpdateTime;
        if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
            // fsync() to ensure that current progress has been flushed to disk,
            // so we can always resume based on latest database information.
            outFd.sync();
            //mInfoDelta.writeToDatabaseOrThrow();
            mInfoDelta.writeToDatabaseWithoutModifyTime();
            mLastUpdateBytes = currentBytes;
            mLastUpdateTime = now;
        }
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadThread.java
總共做了兩件事,第一乐尊,調(diào)用outFd.sync強(qiáng)制所有系統(tǒng)緩沖區(qū)與基礎(chǔ)設(shè)備同步戚丸,第二調(diào)用mInfoDelta的writeToDatabaseWithoutModifyTime去更新數(shù)據(jù)庫操作,即將當(dāng)前進(jìn)度扔嵌,下載了多少update到數(shù)據(jù)庫限府。

Step 13:DownloadInfoDelta.writeToDatabaseWithoutModifyTime

    public void writeToDatabaseWithoutModifyTime() throws StopRequestException {
            final ContentValues values = new ContentValues();

            values.put(Downloads.Impl.COLUMN_URI, mUri);
            values.put(Downloads.Impl._DATA, mFileName);
            values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
            values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
            values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
            values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
            values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
            values.put(Constants.ETAG, mETag);

            values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);

            if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
                    values, Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) {
                throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
            }
        }
     
    }

這個(gè)方法定義在packages/providers/DownloadProvider/src/com/android/providers/downloads/DownloadThread.java
DownloadInfoDelta是DownloadThread的一個(gè)內(nèi)部類,主要用于更新數(shù)據(jù)庫進(jìn)度操作痢缎,這個(gè)方法中此時(shí)uri為"content://downloads/all_downloads/id",對(duì)應(yīng)DownloadProvider的update方法去更新數(shù)據(jù)庫胁勺,而此時(shí)又會(huì)回調(diào)至DowbloadService中的DownloadManagerContentObserver監(jiān)聽中,因?yàn)榇藭r(shí)對(duì)應(yīng)uri數(shù)據(jù)庫內(nèi)容已經(jīng)改變独旷。至此署穗,整個(gè)updateLocked方法執(zhí)行完畢。

簡(jiǎn)單分析DownloadManagerContentObserver內(nèi)容嵌洼,可以看出這個(gè)目的還是保證了下載的連續(xù)性案疲,只要每次有下載數(shù)據(jù)更新,則會(huì)循環(huán)檢測(cè)麻养,以確保下載任務(wù)的連續(xù)性褐啡。

 private class DownloadManagerContentObserver extends ContentObserver {
        public DownloadManagerContentObserver() {
            super(new Handler());
        }

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

至此,整個(gè)下載過程已經(jīng)結(jié)束鳖昌,至于UI界面的更新情況备畦,則只需要監(jiān)聽數(shù)據(jù)庫中的數(shù)據(jù)變化,或者在有下載任務(wù)時(shí)候许昨,間隔一段時(shí)間去數(shù)據(jù)庫查詢進(jìn)度信息懂盐,更新進(jìn)度即可。

對(duì)于下載界面糕档,自4.4之后莉恼,都是BrowserActivity->DownloadList->DocumentActivity,而且對(duì)于DocumentUI正是采用的一段時(shí)間查詢數(shù)據(jù)庫速那,更新的方式俐银,這里我們也不討論了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末琅坡,一起剝皮案震驚了整個(gè)濱河市悉患,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌榆俺,老刑警劉巖售躁,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異茴晋,居然都是意外死亡陪捷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門诺擅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來市袖,“玉大人,你說我怎么就攤上這事〔缘” “怎么了酒觅?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長微峰。 經(jīng)常有香客問我舷丹,道長,這世上最難降的妖魔是什么蜓肆? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任颜凯,我火速辦了婚禮,結(jié)果婚禮上仗扬,老公的妹妹穿的比我還像新娘症概。我一直安慰自己,他們只是感情好早芭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布彼城。 她就那樣靜靜地躺著,像睡著了一般逼友。 火紅的嫁衣襯著肌膚如雪精肃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天帜乞,我揣著相機(jī)與錄音司抱,去河邊找鬼。 笑死黎烈,一個(gè)胖子當(dāng)著我的面吹牛习柠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播照棋,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼资溃,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了烈炭?” 一聲冷哼從身側(cè)響起溶锭,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎符隙,沒想到半個(gè)月后趴捅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡霹疫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年拱绑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丽蝎。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猎拨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情红省,我是刑警寧澤额各,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站类腮,受9級(jí)特大地震影響臊泰,放射性物質(zhì)發(fā)生泄漏蛉加。R本人自食惡果不足惜蚜枢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望针饥。 院中可真熱鬧厂抽,春花似錦、人聲如沸丁眼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽苞七。三九已至藐守,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蹂风,已是汗流浹背卢厂。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留惠啄,地道東北人慎恒。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像撵渡,于是被迫代替她去往敵國和親融柬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容