就OTA包下载完成丢失问题对DownloadProvider源码分析

OTA下载update.zip包找不到了,查看代码分析后解决比较简单,由此拓展对DownloadProvider这个模块进行分析

分析流程

  • OTA代码

    private long download(){
        ...
        DownloadManager.Request request = new DownloadManager.Request (Uri.parse(url));
        request.setTitle(title);
        request.setDescription(dec);
        request.setShowRunningNotification(show);
        request.setVisibleInDownloadsUi(false);
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);  
        return manager.enqueue(request);
    }
    

应用层代码很简单,通过(DownloadManager)getSystemService(DOWNLOAD_SERVICE)得到manager,将request入队列,进行下载

  • DownloadManager.java

    public long enqueue(Request request) {
        ContentValues values = request.toContentValues(mPackageName);
        Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);   /*public static final Uri CONTENT_URI = Uri.parse("content://downloads/my_downloads");*/
        long id = Long.parseLong(downloadUri.getLastPathSegment());
        return id;
    }
    

request.toContentValue对于request转换为values,在利用内容提供者机制进行数据插入。

  • DownloadProvider.java
    @Override
    public Uri insert(final Uri uri, final ContentValues values) {
        checkInsertPermissions(values);
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    ...//对传入了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);

        final String callingPackage = getPackageForUid(Binder.getCallingUid());
        if (callingPackage == null) {
            Log.e(Constants.TAG, "Package does not exist for calling uid");
            return null;
        }
        grantAllDownloadsPermission(callingPackage, rowID);
        notifyContentChanged(uri, match);
        final long token = Binder.clearCallingIdentity();
        try {
            Helpers.scheduleJob(getContext(), rowID);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
        if (values.getAsInteger(COLUMN_DESTINATION) == DESTINATION_NON_DOWNLOADMANAGER_DOWNLOAD
                && values.getAsInteger(COLUMN_MEDIA_SCANNED) == 0) {
            DownloadScanner.requestScanBlocking(getContext(), rowID, values.getAsString(_DATA),
                    values.getAsString(COLUMN_MIME_TYPE));
        }

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

处理完values,将插入数据库,然后授予Uri权限,以及Helpers.scheduleJob开启一个下载线程服务

  • Helpers.java

    public static void scheduleJob(Context context, long downloadId) {
        final boolean scheduled = scheduleJob(context,
                DownloadInfo.queryDownloadInfo(context, downloadId));
        if (!scheduled) {
            // If we didn't schedule a future job, kick off a notification
            // update pass immediately
            getDownloadNotifier(context).update();
        }
    }
    
    public static boolean scheduleJob(Context context, DownloadInfo info) {
        final JobScheduler scheduler = context.getSystemService(JobScheduler.class);
        final int jobId = (int) info.mId;
        scheduler.cancel(jobId);
    
        // Skip scheduling if download is paused or finished
        if (!info.isReadyToSchedule()) return false;
    
        final JobInfo.Builder builder = new JobInfo.Builder(jobId,
                new ComponentName(context, DownloadJobService.class));
    
        // When this download will show a notification, run with a higher
        // priority, since it's effectively a foreground service
        if (info.isVisible()) {
            builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP);
            builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND);
        }
    
        // We might have a backoff constraint due to errors
        final long latency = info.getMinimumLatency();
        if (latency > 0) {
            builder.setMinimumLatency(latency);
        }
    
        // We always require a network, but the type of network might be further
        // restricted based on download request or user override
        builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes));
    
        if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) {
            builder.setRequiresCharging(true);
        }
        if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) {
            builder.setRequiresDeviceIdle(true);
        }
    
        // If package name was filtered during insert (probably due to being
        // invalid), blame based on the requesting UID instead
        String packageName = info.mPackage;
        if (packageName == null) {
            packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0];
        }
    
        scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG);
        return true;
    }
    

通过DownloadInfo静态方法查询数据库,然后利用JobScheduler任务调度类在规定条件下开启DownloadJobService服务。虽然JobScheduler是在Android5.0开始支持,但是DownloadProvider并没有使用此机制,Android5.1都还是使用Service来实现的。

  • DownloadJobService.java

    @Override
    public boolean onStartJob(JobParameters params) {
        final int id = params.getJobId();
    
        // Spin up thread to handle this download
        final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);
        if (info == null) {
            Log.w(TAG, "Odd, no details found for download " + id);
            return false;
        }
    
        final DownloadThread thread;
        synchronized (mActiveThreads) {
            thread = new DownloadThread(this, params, info);
            mActiveThreads.put(id, thread);
        }
        thread.start();
    
        return true;
    }
    
    @Override
    public boolean onStopJob(JobParameters params) {
        final int id = params.getJobId();
    
        final DownloadThread thread;
        synchronized (mActiveThreads) {
            thread = mActiveThreads.removeReturnOld(id);
        }
        if (thread != null) {
            // If the thread is still running, ask it to gracefully shutdown,
            // and reschedule ourselves to resume in the future.
            thread.requestShutdown();
    
            Helpers.scheduleJob(this, DownloadInfo.queryDownloadInfo(this, id));
        }
        return false;
    }
    

调度任务满足条件在执行时,onStartJob就会开启一个下载线程来解析DownInfo进行下载。

  • DownloadThread.java

    @Override
    public void run() {
        ......
        try{
            mInfoDelta.mStatus = STATUS_RUNNING;
            mInfoDelta.writeToDatabase();
            executeDownload();
            mInfoDelta.mStatus = STATUS_SUCCESS;
        }catch (StopRequestException e) {
            mInfoDelta.mStatus = e.getFinalStatus();
            mInfoDelta.mErrorMsg = e.getMessage();
    
            logWarning("Stop requested with status "
                    + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
                    + mInfoDelta.mErrorMsg);
    
            // Nobody below our level should request retries, since we handle
            // failure counts at this level.
            if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
                throw new IllegalStateException("Execution should always throw final error codes");
            }
    
            // Some errors should be retryable, unless we fail too many times.
            if (isStatusRetryable(mInfoDelta.mStatus)) {
                if (mMadeProgress) {
                    mInfoDelta.mNumFailed = 1;
                } else {
                    mInfoDelta.mNumFailed += 1;
                }
    
                if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
                    final NetworkInfo info = mSystemFacade.getNetworkInfo(mNetwork, mInfo.mUid,
                            mIgnoreBlocked);
                    if (info != null && info.getType() == mNetworkType && info.isConnected()) {
                        // Underlying network is still intact, use normal backoff
                        mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
                    } else {
                        // Network changed, retry on any next available
                        mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
                    }
    
                    if ((mInfoDelta.mETag == null && mMadeProgress)
                            || DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
                        // However, if we wrote data and have no ETag to verify
                        // contents against later, we can't actually resume.
                        mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
                    }
                }
            }
    
            // If we're waiting for a network that must be unmetered, our status
            // is actually queued so we show relevant notifications
            if (mInfoDelta.mStatus == STATUS_WAITING_FOR_NETWORK
                    && !mInfo.isMeteredAllowed(mInfoDelta.mTotalBytes)) {
                mInfoDelta.mStatus = STATUS_QUEUED_FOR_WIFI;
            }
    
        }
        ......
        mJobService.jobFinishedInternal(mParams, false);
    }
    
    private void executeDownload() throws StopRequestException {
        final boolean resuming = mInfoDelta.mCurrentBytes != 0;
    
        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);
        }
    
        boolean cleartextTrafficPermitted = mSystemFacade.isCleartextTrafficPermitted(mInfo.mUid);
        SSLContext appContext;
        try {
            appContext = mSystemFacade.getSSLContextForPackage(mContext, mInfo.mPackage);
        } catch (GeneralSecurityException e) {
            // This should never happen.
            throw new StopRequestException(STATUS_UNKNOWN_ERROR, "Unable to create SSLContext.");
        }
        int redirectionCount = 0;
        while (redirectionCount++ < Constants.MAX_REDIRECTS) {
            // Enforce the cleartext traffic opt-out for the UID. This cannot be enforced earlier
            // because of HTTP redirects which can change the protocol between HTTP and HTTPS.
            if ((!cleartextTrafficPermitted) && ("http".equalsIgnoreCase(url.getProtocol()))) {
                throw new StopRequestException(STATUS_BAD_REQUEST,
                        "Cleartext traffic not permitted for UID " + mInfo.mUid + ": "
                        + Uri.parse(url.toString()).toSafeString());
            }
    
            // Open connection and follow any redirects until we have a useful
            // response with body.
            HttpURLConnection conn = null;
            try {
                // Check that the caller is allowed to make network connections. If so, make one on
                // their behalf to open the url.
                checkConnectivity();
                conn = (HttpURLConnection) mNetwork.openConnection(url);
                conn.setInstanceFollowRedirects(false);
                conn.setConnectTimeout(DEFAULT_TIMEOUT);
                conn.setReadTimeout(DEFAULT_TIMEOUT);
                // If this is going over HTTPS configure the trust to be the same as the calling
                // package.
                if (conn instanceof HttpsURLConnection) {
                    ((HttpsURLConnection)conn).setSSLSocketFactory(appContext.getSocketFactory());
                }
    
                addRequestHeaders(conn, resuming);
    
                final int responseCode = conn.getResponseCode();
                switch (responseCode) {
                    case HTTP_OK:
                        if (resuming) {
                            throw new StopRequestException(
                                    STATUS_CANNOT_RESUME, "Expected partial, but received OK");
                        }
                        parseOkHeaders(conn);
                        transferData(conn);
                        return;
    
                    case HTTP_PARTIAL:
                        if (!resuming) {
                            throw new StopRequestException(
                                    STATUS_CANNOT_RESUME, "Expected OK, but received partial");
                        }
                        transferData(conn);
                        return;
                    ...
        }
            }
        }
    }
    
    private void transferData(HttpURLConnection conn) throws StopRequestException {
    ...
        // Move into place to begin writing
        Os.lseek(outFd, mInfoDelta.mCurrentBytes, OsConstants.SEEK_SET);
        ...
        // Start streaming data, periodically watch for pause/cancel
        // commands and checking disk space as needed.
        transferData(in, out, outFd);
        ...
    }
    
     private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
            throws StopRequestException {
        final byte buffer[] = new byte[Constants.BUFFER_SIZE];
        while (true) {
            if (mPolicyDirty) checkConnectivity();
    
            if (mShutdownRequested) {
                throw new StopRequestException(STATUS_HTTP_DATA_ERROR,
                        "Local halt requested; job probably timed out");
            }
    
            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);
            }
        }
        // Finished without error; verify length if known
        if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
            throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch");
        }
    }
    

简单的HttpURLConnection操作,根据reponseCode判断是首次现在还是断点下载,然后就是网络文件的读取以及本地文件的写入操作,updateProgress函数来更新DownloadProvider的update操作。

线程进入run mInfoDelta.mStatus设置为STATUS_RUNNING,当executeDownload()执行完毕之后改为STATUS_SUCCESS,其中下载过程中可能会遇到异常,在catch中处理,有些异常是可以给予机会retry的。

有DownloadThread run函数可知道开始和结束都将写入数据库。mInfoDelta.writeToDatabase();调用的是DownloadProvider.update.

     @Override
    public int update(final Uri uri, final ContentValues values,
            final String where, final String[] whereArgs) {

        isCompleting = status != null && Downloads.Impl.isStatusCompleted(status);
        //1
        ...
        switch (match) {
            case MY_DOWNLOADS:
            case MY_DOWNLOADS_ID:
            case ALL_DOWNLOADS:
            case ALL_DOWNLOADS_ID:
                if (filteredValues.size() == 0) {
                    count = 0;
                    break;
                }

                final SqlSelection selection = getWhereClause(uri, where, whereArgs, match);
                //2
                count = db.update(DB_TABLE, filteredValues, selection.getSelection(),
                        selection.getParameters());
                if (updateSchedule || isCompleting) {
                    final long token = Binder.clearCallingIdentity();
                    try (Cursor cursor = db.query(DB_TABLE, null, selection.getSelection(),
                            selection.getParameters(), null, null, null)) {
                        final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver,
                                cursor);
                        final DownloadInfo info = new DownloadInfo(context);
                        while (cursor.moveToNext()) {
                            reader.updateFromDatabase(info);
                            if (updateSchedule) {
                                Helpers.scheduleJob(context, info);
                            }
                            //3
                            if (isCompleting) {
                                info.sendIntentIfRequested();
                            }
                        }
                    } finally {
                        Binder.restoreCallingIdentity(token);
                    }
                }
                break;

            default:
                Log.d(Constants.TAG, "updating unknown/invalid URI: " + uri);
                throw new UnsupportedOperationException("Cannot update URI: " + uri);
        }

        notifyContentChanged(uri, match);
        return count;
    }

判断values传入进来参数的状态是否为已经完成,

1.values过滤判断

2.插入数据库表

3.如果是则为1出通过DownloadInfo.sendIntentIfRequested发送广播

  • DownloadInfo.java

    public void sendIntentIfRequested() {
        if (mPackage == null) {
            return;
        }
    
        Intent intent;
        if (mIsPublicApi) {
            intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
            intent.setPackage(mPackage);
            intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
        } else { // legacy behavior
            if (mClass == null) {
                return;
            }
            intent = new Intent(Downloads.Impl.ACTION_DOWNLOAD_COMPLETED);
            intent.setClassName(mPackage, mClass);
            if (mExtras != null) {
                intent.putExtra(Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, mExtras);
            }
            // We only send the content: URI, for security reasons. Otherwise, malicious
            //     applications would have an easier time spoofing download results by
            //     sending spoofed intents.
            intent.setData(getMyDownloadsUri());
        }
        mSystemFacade.sendBroadcast(intent);
    }
    
  • 整个流程分析完了,OTA包下载完成小时,一开始以为是DownloadProvider的问题,后来根据代码跟踪发现,OTA代码里面有一个广播接收器DownloadReceiver

    public void onReceive(Context context, Intent intent) {
        mContext = context;
        manager =(DownloadManager)mContext.getSystemService("download");
        long xmlID = SettingsUtils.getDLXmlId(mContext.getContentResolver());
        long zipID = SettingsUtils.getDLUpdateId(mContext.getContentResolver());
        String md5 = SettingsUtils.getMD5(mContext.getContentResolver());
                
        if(intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)){
            Log.d(TAG, "DownloadManager.ACTION_DOWNLOAD_COMPLETE............");
            Runtime runtime = Runtime.getRuntime();    
            try {
                Process proc = runtime.exec("mv -f /storage/emulated/0/Download/update.zip /data/");
                if (proc.waitFor() != 0) { 
                    //Log.d(TAG, "return command....");
                    DownloadingActivity.DESTINPATH = "/data/";
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            long downId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            if (downId == zipID){
                String filePath = DownloadingActivity.DESTINPATH + DownloadingActivity.UPDATE_NAME;
                if(new File(filePath).exists()){
                    Intent updateIntent = new Intent(mContext, UpdateService.class);
                    updateIntent.putExtra("file_name", filePath);
                    updateIntent.putExtra("show_update_settings", true);
                    updateIntent.putExtra("md5", md5);
                    updateIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    mContext.startService(updateIntent);
                    Log.d(TAG, "mContext.startActivity(updateIntent)......");
                }
            }else if (downId == xmlID){
                
            }
        }else if(intent.getAction().equals(DownloadManager.ACTION_NOTIFICATION_CLICKED)){
            Log.d(TAG, "DownloadManager.ACTION_NOTIFICATION_CLICKED............");
            Intent downIntent = new Intent(mContext, DownloadingActivity.class);
            downIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(downIntent);
        }
    }
    

通过代码一眼就看见不对Process proc = runtime.exec("mv -f /storage/emulated/0/Download/update.zip /data/"); shell命令行,mv 是什么鬼,这不是把下载的包移到别的地方起了吗?据说是从OTA代码是从6.0移植过来的,据说当时要一道data目录,现在针对这个来修改就OK了。我这边就注释掉了mv的操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342