AppUpdate
preface
Version upgrade has been a very common function for APP. For each version iteration of the project and the development of new functions, we need to download the updated version and implement our iteration by installing the new version. Of course, in addition to this way, in fact, there are hot update and hot repair, without the need to install the iteration of the version, and many large projects in the accumulation of a large number of users have also adopted the grayscale release function, the first small scale upgrade trial, in the formal market. Today I just want to talk purely about downloading updates based on the system’s own DownloadManager.
Universal flow chart
It is not easy to draw a picture. This flow chart almost contains all the processes involved in app check for update, such as progress box and failed download popbox in the flow chart. MD5 verification is not necessary for me, and DownloadManager is generally used to download updates in the background. It is a standard process to notify the download completion with the Notification of the system, and then the installation interface will pop up automatically.
Involving knowledge induction
- DownloadManager Specifies the apis for downloading services and using them.
- Dynamic application for Android M runtime permissions, mainly involving read and write permissions of the memory card.
- Android N file access permission, cannot be
file://xxx
To access a file, use the FileProvider Uri formatcontent://xxx
. - Android O permission requests for applications from unknown sources.
- Android Q adds a sandbox and changes the way applications access files on the device’s external storage, and can’t build their own directories on the internal storage
- File MD5 verification, prevent apK download interception tampering and verify the integrity of APK files.
DownloadManager introduces and uses it
introduce
DownloadManager a DownloadManager is a system service that handles long-running HTTP downloads. Clients can request urIs to be downloaded to specific target files. The download manager does the download in the background, takes care of the HTTP interaction and retries the download after a failure or cross-connection change and system restart. The translation always feels bad, here is the official quote (official portal)
The download manager is a system service that handles long-running HTTP downloads. Clients may request that a URI be downloaded to a particular destination file. The download manager will conduct the download in the background, taking care of HTTP interactions and retrying downloads after failures or across connectivity changes and system reboots.
Apps that request downloads through this API should register a broadcast receiver for
ACTION_NOTIFICATION_CLICKED
to appropriately handle when the user clicks on a running download in a notification or from the downloads UI.
Note that the application must have the
Manifest.permission.INTERNET permission
to use this class.
The conceptual advantages of the DownloadManager system are clear: 1. 2. You can specify any download path, or support Android Q 3. 4. Native system download service, do not rely on the third party, compatibility and stability is undoubtedly the best 5. The default already encapsulates system bar notifications, wifi/ mobile/roaming, and other download restrictions
Download the core API
Class/constant/method | introduce |
---|---|
DownloadManager.Query | It is used to query and filter the download process, such as download status and progress |
DownloadManager.Request | Download service some configuration, download address, download path, notification bar configuration, network restrictions, media type, etc |
ACTION_DOWNLOAD_COMPLETE | Broadcast intent action sent by the download manager after the download is complete |
ACTION_NOTIFICATION_CLICKED | The download manager sends broadcast intent actions when the user clicks a running download from the system notification or download UI |
ACTION_VIEW_DOWNLOADS | Launch the activity to show the intended action of all downloads, which is the mobile system’s download management interface |
COLUMN_BYTES_DOWNLOADED_SO_FAR | The number of bytes currently downloaded requires the use of the download progress bar |
COLUMN_TOTAL_SIZE_BYTES | The total size of the downloaded file, in bytes, required to download the progress bar |
COLUMN_LOCAL_URI | The downloaded file will be stored in the Uri, note: N before isfile://xxx N, N, Ncontent://xxx |
EXTRA_DOWNLOAD_ID | The download_id is available in the broadcast ACTION_DOWNLOAD_COMPLETE |
COLUMN_REASON | Provides more detailed information about download status |
COLUMN_STATUS | The current download status can be queried by downloadManager.query |
STATUS_PENDING | Download begins |
STATUS_RUNNING | Download in progress |
STATUS_PAUSED | COLUMN_REASON is used to determine the reason for the pause |
STATUS_SUCCESSFUL | Download successful |
STATUS_FAILED | The download failed. The failure will not be retried. The cause can be found in COLUMN_REASON |
enqueue(DownloadManager.Request request) | Start a download service |
getMaxBytesOverMobile(Context context) | Returns the maximum number of mobile network limited downloads |
getMimeTypeForDownloadedFile(long id) | The download_id command is used to query the media type, that is, the format, of the downloaded file |
getRecommendedMaxBytesOverMobile(Context context) | Gets the recommended mobile network download size |
getUriForDownloadedFile(long id) | If the file was downloaded successfully, return the Uri of the file |
openDownloadedFile(long id) | Open the downloaded file and read the file |
query(DownloadManager.Query query) | Download the query |
remove(long… ids) | Cancel the download and delete the file from the download manager |
COLUMN_REASON () : COLUMN_REASON () : COLUMN_REASON () : COLUMN_REASON () : COLUMN_REASON ()
- Download the core code
DownloadManager = (downloadManager) context.getSystemService(context.download_service); clearCurrentTask(); / / download address if it is null, throw an exception String downloadUrl = Objects. RequireNonNull (appUpdate. GetNewVersionUrl ()); Uri uri = Uri.parse(downloadUrl); DownloadManager.Request request = new DownloadManager.Request(uri); / / download and download the complete display notification bar request. SetNotificationVisibility (DownloadManager. Request. VISIBILITY_VISIBLE_NOTIFY_COMPLETED);if(textutils.isempty (appupdate.getSavepath ())) {// Use the default system download path here is in-app/Android /data/ Packages, so compatible with 7.0 request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() +".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS + File.separator + context.getPackageName() + ".apk")));
} else{// Custom download directory, note that this is related to android Q storage permissions, Suggest not to use external.getexternalstoragedirectory () request. SetDestinationInExternalFilesDir (context, appUpdate getSavePath (), context.getPackageName() +".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(appUpdate.getSavePath() + File.separator + context.getPackageName() + ".apk"))); } // Some models (temporarily found Nexus 6P) cannot be downloaded, guess the reason is the default download through the metering network connection, I'm going to dynamically judge ConnectivityManager ConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);if(connectivityManager ! =null){ boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered(); request.setAllowedOverMetered(activeNetworkMetered); } // Set the notification bar's title request.setTitle(getAppName()); // Set the description of the notification bar request.setDescription("Downloading now..."); // Set the media type to apk file request.setMimeType("application/vnd.android.package-archive"); // Start the download, return id lastdowndid = downloadManager.enqueue(request); // Add download listener if you need progress and download statusif(! appUpdate.getIsSlentMode()) { DownloadHandler downloadHandler = new DownloadHandler(this); downloadObserver = new DownloadObserver(downloadHandler, downloadManager, lastDownloadId); context.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true, downloadObserver);
}
Copy the code
- The system’s ContentObserver monitors the progress of locally downloaded files by default, or it can start a timer to check the current progress at regular intervals.
/** * Check the download status */ private voidqueryDownloadStatus() {// Java 7 new try-with-resources, which implement the AutoCloseable interface can automatically close(), Close () try (cursor cursor = downloadManager.query(query)) {if(cursor ! = null && cursor.moveToNext()) { int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)); long totalSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)); long currentSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)); // current progress int mProgress;if(totalSize ! = 0) { mProgress = (int) ((currentSize * 100) / totalSize); }else {
mProgress = 0;
}
Log.d(TAG, String.valueOf(mProgress));
switch (status) {
caseDownloadmanager.status_paused: // Download paused handler.sendEmptyMessage(downloadManager.status_paused); Log.d(TAG,"STATUS_PAUSED");
break;
caseDownloadmanager.status_pending: // Start downloading handler.sendEmptyMessage(downloadManager.status_pending); Log.d(TAG,"STATUS_PENDING");
break;
caseDownloadmanager.status_running: // Downloading, doing nothing Message Message = message.obtain (); message.what = DownloadManager.STATUS_RUNNING; message.arg1 = mProgress; handler.sendMessage(message); Log.d(TAG,"STATUS_RUNNING");
break;
case DownloadManager.STATUS_SUCCESSFUL:
if(! SendEmptyMessage (downloadManager.status_successful); Log.d(TAG,"STATUS_SUCCESSFUL");
}
isEnd = true;
break;
case DownloadManager.STATUS_FAILED:
if(! isEnd) { handler.sendEmptyMessage(DownloadManager.STATUS_FAILED); Log.d(TAG,"STATUS_FAILED");
}
isEnd = true;
break;
default:
break; } } } catch (Exception e) { e.printStackTrace(); }}Copy the code
Android M runtime permissions
Android version 6.0 introduced a new mode of permissions that allows users to manage application permissions directly at run time. This mode allows users to better understand and control permissions, while streamlining installation and automatic updates for application developers. Users can grant or revoke permissions for each application they install.
For applications targeting Android 6.0 (API level 23) or higher, be sure to check and request permissions at run time. To determine if your application has been granted permission, call the new checkSelfPermission() method. To requestPermissions, call the new requestPermissions() method. Even if your application is not targeted at Android6.0 (API level 23), you should test your application in the new permission mode. Official portal
Android M needs to dynamically apply for runtime permissions because the download requires reading and writing files. To view runtime permissions, run the following command through the Terminal of AndroidStudio:
- List permissions and status by group:
$ adb shell pm list permissions -d -g
- Grant or revoke one or more permissions:
$ adb shell pm [grant|revoke] …
- List all permissions:
$ adb shell pm list permissions -s
M runtime permission request snippet:
/** * Determine the storage card permission */ private voidrequestPermission() {/ / permission to determine whether there is access to external storage space int flag = ActivityCompat. CheckSelfPermission (getActivity (), Manifest.permission.WRITE_EXTERNAL_STORAGE);if(flag ! = PackageManager.PERMISSION_GRANTED) {if(ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), The Manifest. Permission. WRITE_EXTERNAL_STORAGE)) {/ / user rejected the permissions, should prompt the user, why need the permissions. Toast.makeText(getActivity(), getResources().getString(R.string.update_permission), Toast.LENGTH_LONG).show(); } / / for authorization requestPermissions (new String [] {the Manifest. Permission. WRITE_EXTERNAL_STORAGE}, 1); }else{// Have permissions, Perform download logic}} @ Override public void onRequestPermissionsResult (int requestCode, @ NonNull String [] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == 1) {
if(grantResults. Length > 0 && grantResults [0] = = PackageManager. PERMISSION_GRANTED) {/ / grant permission, perform download logic}else{// Deny permissions, Toast.maketext (getActivity(), getResources().getString(r.sing.update_Permission), toast.length_long).show(); dismiss(); }}}}Copy the code
Android N file access permission
In order to improve the security of private files, access to the private directories of apps for Android7.0 or higher is restricted (0700). This setting prevents metadata leakage of private files, such as their size or existence. This permission change has multiple side effects:
- File permissions for private files should no longer be extended by the owner for use
MODE_WORLD_READABLE
And/orMODE_WORLD_WRITEABLE
Such an attempt is triggeredSecurityException
.
Note: To date, this restriction cannot be fully enforced. Applications may still use the native API or File API to modify their private directory permissions. However, we strongly oppose any relaxation of private directory permissions
- Passing file://URI outside the package network may leave an inaccessible path for the sink. Therefore, trying to pass the file:// URI fires
FileUriExposedException
. The recommended way to share private file content is to use FileProvider. - DownloadManager no longer shares privately stored files by filename. The old app is accessing
COLUMN_LOCAL_FILENAME
An unreachable path may appear when. Apps for Android7.0 or higher are trying to accessCOLUMN_LOCAL_FILENAME
Triggered whenSecurityException
. Through the use ofDownloadManager.Request.setDestinationInExternalFilesDir()
orDownloadManager.Request.setDestinationInExternalPublicDir()
Older apps with the download location set to public will still be accessibleCOLUMN_LOCAL_FILENAME
But we strongly oppose using this approach. For files exposed by DownloadManager, the preferred method of access is usingContentResolver.openFileDescriptor()
.
Take a look at the code snippet below:
The manifest file
<provider
android:name=".DownloadFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_file_path" />
</provider>
Copy the code
File Storage Configuration
<paths>
<external-path
name="external_storage_root"
path="." />
<files-path
name="files-path"
path="." />
<cache-path
name="cache-path"
path="."/ > <! --/storage/emulated/0/Android/data/... --> <external-files-path name="external_file_path"
path="."/ > <! - app external storage area under the root directory of files Context. GetExternalCacheDir directory in the directory - > < external cache - the path name ="external_cache_path"
path="."/ > <! -- Configure root-path. This way you can read the sd card and some app clone directories. It is said that the app clone has a bug--> <root-path name="root-path"
path="" />
/paths>
Copy the code
The app is installed
File downloadFile = getDownloadFile();
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
intent.setDataAndType(Uri.fromFile(downloadFile), "application/vnd.android.package-archive");
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
boolean allowInstall = context.getPackageManager().canRequestPackageInstalls();
if(! AllowInstall) {// Not allowed to install apps from unknown sources, request permission to install apps from unknown sourcesif(mainPageExtraListener ! = null) { mainPageExtraListener.applyAndroidOInstall(); }return; }} / / Android7.0 after get the uri to use the contentProvider uri apkUri FileProvider. = getUriForFile (context) getApplicationContext (), context.getPackageName() +".fileProvider", downloadFile);
//Granting Temporary Permissions to a URI
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
Copy the code
Android O about apps from unknown sources
Applications for 8.0 need to declare REQUEST_INSTALL_PACKAGES permission in androidmanifest.xml. Otherwise, in-app upgrades cannot be performed
The manifest file
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
Copy the code
Permissions to detect
*/ RequiresApi(API = build.version_codes.o) @override public void */ RequiresApi(API = build.version_codes.o) @override public voidapplyAndroidOInstall() {/ / request to install unknown sources application permissions ActivityCompat. RequestPermissions (this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); // 8.0 permission request result callbackif(requestCode == INSTALL_PACKAGES_REQUESTCODE) {// Authorization succeedsif(grantResults. Length > 0 && grantResults [0] = = PackageManager. PERMISSION_GRANTED) {/ / installation apk logic... }else{// Authorization failed, leading user to unknown application installation interfaceif(android. OS. Build. VERSION. SDK_INT. > = android OS. Build. VERSION_CODES. O) {/ / note that this is a new API Uri packageUri = 8.0 Uri. Parse ("package:"+ getPackageName()); Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri); startActivityForResult(intent, GET_UNKNOWN_APP_SOURCES); } } } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); 8.0 Application Settings interface unknown Installation open source return timeif (requestCode == GET_UNKNOWN_APP_SOURCES) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
boolean allowInstall = getPackageManager().canRequestPackageInstalls();
if(allowInstall) {// Execute the logic to install the app... }else{// Deny permission logic... Toast.makeText(MainActivity.this,"You have refused to install an app from an unknown source, the app cannot be updated for now!",Toast.LENGTH_SHORT).show(); }}}}Copy the code
Android Q storage changes
Android Q is currently in Beta, but the biggest change is to further protect user privacy. It provides a separate sandbox for each application on external storage devices. Files created by an application path are stored in the sandbox directory of the application. For download, the file must be saved locally, but AndroidQ uses partition storage, resulting in: With getExternalStoragePublicDirectory external.getexternalstoragedirectory () () to read and write permissions, the user has read and write access at the same time, can not be in the construction of internal storage is wanton own directory, and it is also easier to manage, You can also delete the data and files completely when uninstalling the application.
if(textutils.isempty (appupdate.getSavepath ())) {// Use the default system download path here is in-app/Android /data/ Packages, so compatible with 7.0 request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() +".apk");
} else{// Custom download directory, note that this is related to android Q storage permissions, Suggest not to use external.getexternalstoragedirectory () request. SetDestinationInExternalFilesDir (context, appUpdate getSavePath (), context.getPackageName() +".apk"); / / clear the local cache file deleteApkFile (Objects. RequireNonNull (context. GetExternalFilesDir (appUpdate. GetSavePath ()))); }Copy the code
Through setDestinationInExternalFilesDir () to store files and getExternalFilesDir () to obtain documents, can completely avoid the Android Q make restrictions for storage.
File MD5 Verification
If the system DownloadManager is used to implement the update, I think there is no need for verification. Of course, IF the downloaded file is afraid of being tampered with or incomplete, MD5 verification is recommended. There are several points about the role of MD5:
- Used to verify whether the APK file signature is consistent, preventing download interception and tampering
- Used to verify file size integrity
Take a look at the code snippet below:
/** * Check the MD5 validity of the file. If the validity is inconsistent, the file cannot be installed. ** @param MD5 value of the file returned by the MD5 server * @param file Downloaded APK file * @return trueThe MD5 authentication succeedsfalse*/ public static Boolean checkFileMd5(String md5, File File) {if (TextUtils.isEmpty(md5)) {
return false;
}
String md5OfFile = getFileMd5ToString(file);
if (TextUtils.isEmpty(md5OfFile)) {
return false;
}
return md5.equalsIgnoreCase(md5OfFile);
}
/**
* Return the MD5 of file.
*
* @param file The file.
* @return the md5 of file
*/
private static String getFileMd5ToString(final File file) {
return bytes2HexString(getFileMd5(file));
}
private static final char[] HEX_DIGITS =
{'0'.'1'.'2'.'3'.'4'.'5'.'6'.'7'.'8'.'9'.'A'.'B'.'C'.'D'.'E'.'F'};
private static String bytes2HexString(final byte[] bytes) {
if (bytes == null) {
return "";
}
int len = bytes.length;
if (len <= 0) {
return "";
}
char[] ret = new char[len << 1];
for (int i = 0, j = 0; i < len; i++) {
ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
}
return new String(ret);
}
/**
* Return the MD5 of file.
*
* @param file The file.
* @return the md5 of file
*/
private static byte[] getFileMd5(final File file) {
if (file == null) {
return null;
}
DigestInputStream dis = null;
try {
FileInputStream fis = new FileInputStream(file);
MessageDigest md = MessageDigest.getInstance("MD5");
dis = new DigestInputStream(fis, md);
byte[] buffer = new byte[1024 * 256];
while (true) {
if (dis.read(buffer) <= 0) {
break;
}
}
md = dis.getMessageDigest();
return md.digest();
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
} finally {
try {
if (dis != null) {
dis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
Copy the code
The last
About version update about so many knowledge points, relatively simple, but very fragmented, if you want to understand the detailed content, you can download the source code to view oh, see my open source address AppUpdate, the library after long-term verification, stability is OK, if you have a good idea, directly mention issues.
The current functions of this library
- Compatible with AndroidX, the project has been migrated to AndroidX
- Android M handles runtime permissions on stored files
- For Android N, Android enhances the security of file access by using FileProvider to access files
- For Android O, add tips for installing apps from unknown sources
- For Android Q, adding a sandbox to Q changes the way applications access files on the device’s external storage, such as an SD card
- Support silent download, download automatically pop up installation
- Support download progress monitoring and download failure prompt
- Mandatory update is supported. Applications cannot be used if they are not updated
- Supports tamper-proof and integrity verification of MD5 files
- Supports custom update prompt interface
- Download failed. You can download the file from the system browser