“强制升级”会中断用户操做,阻碍正常使用,看似是一个不光彩的行为,可是智者千虑必有一失,咱们没法保证 App 的正确性,在某些紧急状况下,强制升级仍是很是必要的,并且接入的时间越早越好。html
有赞微商城 App 早期版本只提供了一个更新提示的对话框,并不会强制用户更新。随着后端网关升级,一些老的服务须要下线,可是新版本到达率并不理想,继续维护老接口带来必定成本,并且新功能也没法触及用户。java
为了提高版本到达率,咱们从新梳理了强制升级的逻辑。android
升级过程当中首先要保证 apk 的下载成功率,下载完成以后要及时弹出安装页面,为了防止下载失败,也要提供市场下载的选项,这样必定程度上也能保证升级以后渠道的一致性。git
更新对话框须要展现标题、内容和动做按钮。github
状态栏下载通知须要展现应用名字和描述。后端
业务方须要提供的参数:app
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); }
为了提升下载成功率,咱们使用了系统 Service - DownloadManager,由于是独立进程,不会增长 App 占用的系统开销。ide
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 来接收下载完成的广播,这样即便 App 进程被杀死,依然能够安装界面。gradle
<receiver android:name=".DownloadReceiver" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/> </intent-filter> </receiver>
接收到广播以后,弹出安装界面。ui
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
。
关于 package name 和 application id 的区别,能够参考 http://blog.csdn.net/feelang/...