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的操作。