Android 强制升级逻辑和实现

阳光2 8年前
   <p>“强制升级”会中断用户操作,阻碍正常使用,看似是一个不光彩的行为,但是智者千虑必有一失,我们无法保证 App 的正确性,在某些紧急情况下,强制升级还是非常必要的,而且接入的时间越早越好。</p>    <p>有赞微商城 App 早期版本只提供了一个更新提示的对话框,并不会强制用户更新。随着后端网关升级,一些老的服务需要下线,但是新版本到达率并不理想,继续维护老接口带来一定成本,而且新功能也无法触及用户。</p>    <p>为了提升版本到达率,我们重新梳理了强制升级的逻辑。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e9ba9db3cba2344fe84b85cedc66e049.png"></p>    <p>升级过程中首先要保证 apk 的 <strong>下载成功率</strong> ,下载完成之后要及时弹出安装页面,为了防止下载失败,也要提供 <strong>市场下载</strong> 的选项,这样一定程度上也能保证升级之后渠道的一致性。</p>    <ul>     <li> <p>更新对话框需要展示标题、内容和动作按钮。</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/23b364107185c094caaecc70356dde7f.png"></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b4c8a392f59009da496f8b2870c842d9.png"></p>    <ul>     <li> <p>状态栏下载通知需要展示应用名字和描述。</p> </li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/96987fcb36feea1e413963893ad937cd.png"></p>    <h2>构造参数</h2>    <p>业务方需要提供的参数:</p>    <pre>  <code class="language-java">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);      }</code></pre>    <h2>使用 DownloadManager 下载 apk</h2>    <p>为了提高下载成功率,我们使用了系统 Service - DownloadManager,因为是独立进程,不会增加 App 占用的系统开销。</p>    <pre>  <code class="language-java">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);  }</code></pre>    <h2>注册监听下载完成的 Receiver</h2>    <p>我们通过一个全局的 Receiver 来接收下载完成的广播,这样即使 App 进程被杀死,依然可以安装界面。</p>    <pre>  <code class="language-java"><receiver      android:name=".DownloadReceiver"      android:enabled="true"      android:exported="true">      <intent-filter>          <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>      </intent-filter>  </receiver></code></pre>    <p>接收到广播之后,弹出安装界面。</p>    <pre>  <code class="language-java">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);  }</code></pre>    <p>注意此处有坑,在 SDK >= 24 的系统中,Intent 不允许携带 file:// 格式的数据,只能通过 provider 的形式共享数据。</p>    <p>所以我们还需要注册一个 FileProvider 。</p>    <pre>  <code class="language-java"><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></code></pre>    <p>${applicationId}$ 是 AndroidManifest.xml 中的占位符,gradle 会进行替换。</p>    <pre>  <code class="language-java">android:authorities="${applicationId}.provider"</code></pre>    <p>对应 Java 代码:</p>    <pre>  <code class="language-java">FileProvider.getUriForFile(context, context.getPackageName() + ".provider", new File(uri.getPath()))</code></pre>    <p>注意:Java 代码中 getPackageName() 的返回值是 ApplicationId 。</p>    <p> </p>    <p>来自:https://youzanmobile.github.io/2017/03/01/zan-app-updater/</p>    <p> </p>