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); }
應(yīng)用層代碼很簡單,通過(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轉(zhuǎn)換為values,在利用內(nèi)容提供者機制進行數(shù)據(jù)插入。
- DownloadProvider.java
@Override
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
...//對傳入了values進行過濾以及內(nèi)部處理
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,將插入數(shù)據(jù)庫,然后授予Uri權(quán)限,以及Helpers.scheduleJob開啟一個下載線程服務(wù)
-
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靜態(tài)方法查詢數(shù)據(jù)庫,然后利用JobScheduler任務(wù)調(diào)度類在規(guī)定條件下開啟DownloadJobService服務(wù)。雖然JobScheduler是在Android5.0開始支持,但是DownloadProvider并沒有使用此機制,Android5.1都還是使用Service來實現(xiàn)的。
-
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; }
調(diào)度任務(wù)滿足條件在執(zhí)行時,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操作,根據(jù)reponseCode判斷是首次現(xiàn)在還是斷點下載,然后就是網(wǎng)絡(luò)文件的讀取以及本地文件的寫入操作,updateProgress函數(shù)來更新DownloadProvider的update操作。
線程進入run mInfoDelta.mStatus設(shè)置為STATUS_RUNNING,當executeDownload()執(zhí)行完畢之后改為STATUS_SUCCESS,其中下載過程中可能會遇到異常,在catch中處理,有些異常是可以給予機會retry的。
有DownloadThread run函數(shù)可知道開始和結(jié)束都將寫入數(shù)據(jù)庫。mInfoDelta.writeToDatabase();調(diào)用的是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傳入進來參數(shù)的狀態(tài)是否為已經(jīng)完成,
1.values過濾判斷
2.插入數(shù)據(jù)庫表
3.如果是則為1出通過DownloadInfo.sendIntentIfRequested發(fā)送廣播
-
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的問題,后來根據(jù)代碼跟蹤發(fā)現(xiàn),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 是什么鬼,這不是把下載的包移到別的地方起了嗎?據(jù)說是從OTA代碼是從6.0移植過來的,據(jù)說當時要一道data目錄,現(xiàn)在針對這個來修改就OK了。我這邊就注釋掉了mv的操作。