Android 強制升級邏輯和實現
“強制升級”會中斷用戶操作,阻礙正常使用,看似是一個不光彩的行為,但是智者千慮必有一失,我們無法保證 App 的正確性,在某些緊急情況下,強制升級還是非常必要的,而且接入的時間越早越好。
有贊微商城 App 早期版本只提供了一個更新提示的對話框,并不會強制用戶更新。隨著后端網關升級,一些老的服務需要下線,但是新版本到達率并不理想,繼續維護老接口帶來一定成本,而且新功能也無法觸及用戶。
為了提升版本到達率,我們重新梳理了強制升級的邏輯。
升級過程中首先要保證 apk 的 下載成功率 ,下載完成之后要及時彈出安裝頁面,為了防止下載失敗,也要提供 市場下載 的選項,這樣一定程度上也能保證升級之后渠道的一致性。
-
更新對話框需要展示標題、內容和動作按鈕。
-
狀態欄下載通知需要展示應用名字和描述。
構造參數
業務方需要提供的參數:
public class AppUpdater {
public static class Builder {
private Context context;
private String url; // apk 下載鏈接
private String title; // 更新對話框 title
private String content; // 更新內容
private boolean force; // 是否強制更新
private String app; // app 名字
private String description; // app 描述
}
private AppUpdater(final Builder builder) {
this.builder = builder;
}
public void update() {
Intent intent = new Intent(builder.context, DownloadActivity.class);
intent.putExtra(DownloadActivity.EXTRA_STRING_APP_NAME, builder.app);
intent.putExtra(DownloadActivity.EXTRA_STRING_URL, builder.url);
intent.putExtra(DownloadActivity.EXTRA_STRING_TITLE, builder.title);
intent.putExtra(DownloadActivity.EXTRA_STRING_CONTENT, builder.content);
intent.putExtra(DownloadActivity.EXTRA_STRING_DESCRIPTION, builder.description);
intent.putExtra(DownloadActivity.EXTRA_BOOLEAN_FORCE, builder.force);
builder.context.startActivity(intent);
}
使用 DownloadManager 下載 apk
為了提高下載成功率,我們使用了系統 Service - DownloadManager,因為是獨立進程,不會增加 App 占用的系統開銷。
private void downloadApk() {
if (TextUtils.isEmpty(downloadUrl)) return;
// check dir
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
if (!path.exists() && !path.mkdirs()) {
Toast.makeText(this, String.format(getString(R.string.app_updater_dir_not_found),
path.getPath()), Toast.LENGTH_SHORT).show();
return;
}
/** construct request */
final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl));
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE
| DownloadManager.Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,
appName + ".apk");
if (!TextUtils.isEmpty(appName)) {
request.setTitle(appName);
}
if (!TextUtils.isEmpty(description)) {
request.setDescription(description);
} else {
request.setDescription(downloadUrl);
}
/** start downloading */
downloadId = downloadManager.enqueue(request);
setStatus(STATUS_DOWNLOADING);
}
注冊監聽下載完成的 Receiver
我們通過一個全局的 Receiver 來接收下載完成的廣播,這樣即使 App 進程被殺死,依然可以安裝界面。
<receiver
android:name=".DownloadReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</receiver>
接收到廣播之后,彈出安裝界面。
private void installApk(final Context context, final Uri uri) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri apkUri = uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider",
new File(uri.getPath()));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
context.startActivity(intent);
}
注意此處有坑,在 SDK >= 24 的系統中,Intent 不允許攜帶 file:// 格式的數據,只能通過 provider 的形式共享數據。
所以我們還需要注冊一個 FileProvider 。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
${applicationId}$ 是 AndroidManifest.xml 中的占位符,gradle 會進行替換。
android:authorities="${applicationId}.provider"
對應 Java 代碼:
FileProvider.getUriForFile(context, context.getPackageName() + ".provider", new File(uri.getPath()))
注意:Java 代碼中 getPackageName() 的返回值是 ApplicationId 。
來自:https://youzanmobile.github.io/2017/03/01/zan-app-updater/