美文网首页Android 知识程序员来到JavaEE
Android app 在线更新那点事儿(适配Android6.

Android app 在线更新那点事儿(适配Android6.

作者: 依然范特稀西 | 来源:发表于2018-04-16 21:08 被阅读10737次
    一、前言

    app在线更新是一个比较常见需求,新版本发布时,用户进入我们的app,就会弹出更新提示框,第一时间更新新版本app。在线更新分为以下几个步骤:

     1, 通过接口获取线上版本号,versionCode
     2, 比较线上的versionCode 和本地的versionCode,弹出更新窗口
     3, 下载APK文件(文件下载)
     4,安装APK
    

    在线更新就上面几个步骤,前2步比较简单,重要的就是后2个步骤,而由于Android 各个版本对权限和隐私的收归和保护,因此,会出现各种的适配问题,因此本文就总结一下app 在线更新方法和遇到的一些适配问题。

    二、apk 下载

    apk下载其实就是文件下载,而文件下载有很多方式:

      1,很多三方框架都有文件上传下载功能,可以借助三方框架(比如Volley,OkHttp)
      2,也可以开启一个线程去下载,(可以用IntentService)
      3,最简单的一种方式:Android SDK 其实给我们提供了下载类DownloadManager,只需要简单的配置项设置,就能轻松实现下载功能。
    

    本文就用第三种方式,用 DownloadManager 来下载apk。

    1. 使用DownloadManager 下载apk

    DownloadManager 是SDK 自带的,大概流程如下:

    (1)创建一个Request,进行简单的配置(下载地址,和文件保存地址等)
    (2)下载完成后,系统会发送一个下载完成的广播,我们需要监听广播。
    (3)监听到下载完成的广播后,根据id查找下载的apk文件
    (4)在代码中执行apk安装。

    public void downloadApk(String apkUrl, String title, String desc) {
           // fix bug : 装不了新版本,在下载之前应该删除已有文件
           File apkFile = new File(weakReference.get().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
    
           if (apkFile != null && apkFile.exists()) {
            
               apkFile.delete();
              
           }
    
           DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
           //设置title
           request.setTitle(title);
           // 设置描述
           request.setDescription(desc);
           // 完成后显示通知栏
           request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
           request.setDestinationInExternalFilesDir(weakReference.get(), Environment.DIRECTORY_DOWNLOADS, "test.apk");
           //在手机SD卡上创建一个download文件夹
           // Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
           //指定下载到SD卡的/download/my/目录下
           // request.setDestinationInExternalPublicDir("/codoon/","test.apk");
    
           request.setMimeType("application/vnd.android.package-archive");
           //记住reqId
           mReqId = mDownloadManager.enqueue(request);
       }
    

    如上代码所示,首先构建一个Request,设置下载地址,标题、描述、apk存放目录等,最后,调用mDownloadManager.enqueue(request) 开始下载。

    注意:这里我们需要记住这个mReqId,因为下载完成之后,我们需要根据这个ID 去查找apk文件,然后安装apk.

    2.更新下载进度

    下载文件,我们一般需要知道下载的进度,在界面给用户一个友好的提示,app 更新也是一样,我们需要在界面上显示当前下载进度和总进度,让用户知道大概会等待多久。那么如果获取下载进度呢?

    在下载之前,我们需要在Activity 中注册一个Observer,就是一个观察者,当下载进度变化的时候,就会通知观察者,从而更新进度。步骤如下:

    1, 首先我们先定义一个观察者DownloadChangeObserver来观察下载进度
    2,在DownloadChangeObserver 中更新UI进度,给用户提示
    3,下载之前,在Activity 中注册Observer
    

    具体代码如下:
    DownloadChangeObserver.class:

     class DownloadChangeObserver extends ContentObserver {
    
            /**
             * Creates a content observer.
             *
             * @param handler The handler to run {@link #onChange} on, or null if none.
             */
            public DownloadChangeObserver(Handler handler) {
                super(handler);
            }
    
            @Override
            public void onChange(boolean selfChange) {
                super.onChange(selfChange);
                updateView();
            }
        }
    

    updateView()方法中,查询下载进度。

     private void updateView() {
            int[] bytesAndStatus = new int[]{0, 0, 0};
            DownloadManager.Query query = new DownloadManager.Query().setFilterById(mReqId);
            Cursor c = null;
            try {
                c = mDownloadManager.query(query);
                if (c != null && c.moveToFirst()) {
                    //已经下载的字节数
                    bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    //总需下载的字节数
                    bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    //状态所在的列索引
                    bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }
    
            if (mUpdateListener != null) {
                mUpdateListener.update(bytesAndStatus[0], bytesAndStatus[1]);
            }
    
            Log.i(TAG, "下载进度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1] + "");
        }
    

    根据前面我们记录的ID去查询进度,代码中已经注释了,不再多讲。

    要想获取到进度,在下载之前,还得先注册DownloadChangeObserver,代码如下:

    weakReference.get().getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,
                    mDownLoadChangeObserver);
    

    3. 获取下载结果

    DownloadManager在下载完成之后,会发送一个下载完成的广播DownloadManager.ACTION_DOWNLOAD_COMPLETE,我们只需要监听这个广播,收到广播后, 获取apk文件安装。

    定义一个广播DownloadReceiver

    class DownloadReceiver extends BroadcastReceiver {
    
            @Override
            public void onReceive(final Context context, final Intent intent) {
            //  安装APK
            long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    
            Logger.e(TAG, "收到广播");
            Uri uri;
            Intent intentInstall = new Intent();
            intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intentInstall.setAction(Intent.ACTION_VIEW);
    
            if (completeDownLoadId == mReqId) {
               uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
             }
    
             intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
             context.startActivity(intentInstall);
            }
                
        }
    

    在下载之前注册广播

     // 注册广播,监听APK是否下载完成
      weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
    

    通过上面的几个步骤,基本上就完成app在线更新功能,在Android 6.0以下可以正常运行。但是别忙,本文还没有结束,Android每一个版本都有一些改动,导致我们需要适配不同的版本,不然的话,就会出问题,结下来就看一下Android 6.0,7.0,8.0 的相关适配。

    三、适配Android 6.0

    通过前面讲的几个步骤,app 在线更新在6.0以下已经可以正常运行,在Android6.0上,安装的时候会报出以下错误:

    Android6.0错误.png
    Caused by:
    5 android.content.ActivityNotFoundException:No Activity found to handle Intent { act=android.intent.action.VIEW typ=application/vnd.android.package-archive flg=0x10000000 }
    

    为什么会报上面的错误,经过debug发现,在Android6.0以下和Android6.0上,通过DownloadManager 获取到的Uri不一样。

    区别如下:(1)Android 6.0,getUriForDownloadedFile得到 值为: content://downloads/my_downloads/10
    (2) Android6.0以下,getUriForDownloadedFile得到的值为:file:///storage/emulated/0/Android/data/packgeName/files/Download/xxx.apk

    可以看到,Android6.0得到的apk地址为:content:// 开头的一个地址,安装的时候就会报上面的错误。怎么解决呢?经过查找资料找到了解决办法:

       //通过downLoadId查询下载的apk,解决6.0以后安装的问题
        public static File queryDownloadedApk(Context context, long downloadId) {
            File targetApkFile = null;
            DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
    
            if (downloadId != -1) {
                DownloadManager.Query query = new DownloadManager.Query();
                query.setFilterById(downloadId);
                query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
                Cursor cur = downloader.query(query);
                if (cur != null) {
                    if (cur.moveToFirst()) {
                        String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                        if (!TextUtils.isEmpty(uriString)) {
                            targetApkFile = new File(Uri.parse(uriString).getPath());
                        }
                    }
                    cur.close();
                }
            }
            return targetApkFile;
        }
    

    代码如上所示,不通过getUriForDownloadedFile去获取Uri,通过DownloadManager.COLUMN_LOCAL_URI 这个字段去获取apk地址。

    适配Android 6.0后,安装apk 的代码如下:

     /**
         * @param context
         * @param intent
         */
        private void installApk(Context context, Intent intent) {
            long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    
            Logger.e(TAG, "收到广播");
            Uri uri;
            Intent intentInstall = new Intent();
            intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intentInstall.setAction(Intent.ACTION_VIEW);
    
            if (completeDownLoadId == mReqId) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
                    uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
                } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
                    File apkFile = queryDownloadedApk(context, completeDownLoadId);
                    uri = Uri.fromFile(apkFile);
                } 
    
                // 安装应用
                Logger.e("zhouwei", "下载完成了");
    
                intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
                context.startActivity(intentInstall);
            }
        }
    
    四、适配Android 7.0

    刚适配完6.0,在7.0以上的机子上又出问题了,为什么呢?因为在Android 7.0上,对文件的访问权限作出了修改,不能在使用file://格式的Uri 访问文件 ,Android 7.0提供 FileProvider,应该使用这个来获取apk地址,然后安装apk。如下进行简单的适配:

    (1) 在res 目录下,新建一个xml 文件夹,在xml 下面创建一个文件provider_paths文件:

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <external-path
            name="external"
            path="" />
        <external-files-path
            name="Download"
            path="" />
    </paths>
    

    (2) 在AndroidManifest.xml清单文件中申明Provider:

    <!-- Android 7.0 照片、APK下载保存路径-->
            <provider
                android:name="android.support.v4.content.FileProvider"
                android:authorities="packgeName.fileProvider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/provider_paths" />
            </provider>
    
    

    (3) Android 7.0上的文件地址获取:

     uri = FileProvider.getUriForFile(context,
                            "packageNam.fileProvider",
                            new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));
    

    好了,就这样7.0适配工作就完成了,适配后的安装代码如下:

     /**
         * @param context
         * @param intent
         */
        private void installApk(Context context, Intent intent) {
            long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    
            Logger.e(TAG, "收到广播");
            Uri uri;
            Intent intentInstall = new Intent();
            intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intentInstall.setAction(Intent.ACTION_VIEW);
    
            if (completeDownLoadId == mReqId) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
                    uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
                } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
                    File apkFile = queryDownloadedApk(context, completeDownLoadId);
                    uri = Uri.fromFile(apkFile);
                } else { // Android 7.0 以上
                    uri = FileProvider.getUriForFile(context,
                            "packgeName.fileProvider",
                            new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "xxx.apk"));
                    intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }
    
                // 安装应用
                Logger.e("zhouwei", "下载完成了");
    
                intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
                context.startActivity(intentInstall);
            }
        }
    

    注意:把上面的packageNam 换成你自己的包名,把xxx.apk 换成你自己的apk的名字。
    关于更多FileProvider的东西,这儿就不展开讲了,想要了解的可以看一下鸿洋的文章:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧
    ,讲的很清楚。

    五、适配Android 8.0:未知来源的应用权限

    好特么累,继续适配Android 8.0, 由于没有Android 8.0的手机,一直没有注意,前些天一个华为用户反馈在线更新不了新版本,具体表现就是:apk下载完成,一闪而过,没有跳转到apk安装界面。经过排查,确定了是Android 8.0权限问题。

    Android8.0以上,未知来源的应用是不可以通过代码来执行安装的(在sd卡中找找到apk,手动安装是可以的),未知应用安装权限的开关被除掉,取而代之的是未知来源应用的管理列表,需要列表里面开启你的应用的未知来源的安装权限。Google这么做是为了防止一开始正经的应用后来开始通过升级来做一些不合法的事情,侵犯用户权益。

    知道问题了,我们就适配吧:

    (1) 在清单文件中申明权限:REQUEST_INSTALL_PACKAGES

      <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
    

    (2) 在代码中判断用户是否已经受过权限了,如果已经授权,可以直接安装,如果没有授权,则跳转到授权列表,让用户开启未知来源应用安装权限,开启后,再安装应用。

    在监听apk下载状态的广播中添加如下代码:

    boolean haveInstallPermission;
                // 兼容Android 8.0
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
                    //先获取是否有安装未知来源应用的权限
                    haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();
                    if (!haveInstallPermission) {//没有权限
                        // 弹窗,并去设置页面授权
                        final AndroidOInstallPermissionListener listener = new AndroidOInstallPermissionListener() {
                            @Override
                            public void permissionSuccess() {
                                installApk(context, intent);
                            }
    
                            @Override
                            public void permissionFail() {
                                ToastUtils.shortToast(context, "授权失败,无法安装应用");
                            }
                        };
    
                        AndroidOPermissionActivity.sListener = listener;
                        Intent intent1 = new Intent(context, AndroidOPermissionActivity.class);
                        context.startActivity(intent1);
    
    
                    } else {
                        installApk(context, intent);
                    }
                } else {
                    installApk(context, intent);
                }
    

    因为授权时需要弹框提示,我们用一个Activity来代理创建了一个Activity:AndroidOPermissionActivity 来申请权限,用户点击设置后,跳转到权限设置界面,然后我们再onActivityResult 里判断是都授权成功。

    AndroidOPermissionActivity 代码如下:

    /**
     * 兼容Android 8。0 APP 在线更新,权限申请界面
     * Created by zhouwei on 2018/3/23.
     */
    
    public class AndroidOPermissionActivity extends BaseActivity {
        public static final int INSTALL_PACKAGES_REQUESTCODE = 1;
        private AlertDialog mAlertDialog;
        public static AppDownloadManager.AndroidOInstallPermissionListener sListener;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            // 弹窗
    
            if (Build.VERSION.SDK_INT >= 26) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE);
            } else {
                finish();
            }
    
    
        }
    
        @RequiresApi(api = Build.VERSION_CODES.O)
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            switch (requestCode) {
                case INSTALL_PACKAGES_REQUESTCODE:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        if (sListener != null) {
                            sListener.permissionSuccess();
                            finish();
                        }
                    } else {
                        //startInstallPermissionSettingActivity();
                        showDialog();
                    }
                    break;
    
            }
        }
    
        private void showDialog() {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle(R.string.app_name);
            builder.setMessage("为了正常升级 xxx APP,请点击设置按钮,允许安装未知来源应用,本功能只限用于 xxx APP版本升级");
            builder.setPositiveButton("设置", new DialogInterface.OnClickListener() {
                @RequiresApi(api = Build.VERSION_CODES.O)
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    startInstallPermissionSettingActivity();
                    mAlertDialog.dismiss();
                }
            });
            builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    if (sListener != null) {
                        sListener.permissionFail();
                    }
                    mAlertDialog.dismiss();
                    finish();
                }
            });
            mAlertDialog = builder.create();
            mAlertDialog.show();
        }
    
        @RequiresApi(api = Build.VERSION_CODES.O)
        private void startInstallPermissionSettingActivity() {
            //注意这个是8.0新API
            Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, 1);
        }
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (requestCode == 1 && resultCode == RESULT_OK) {
                // 授权成功
                if (sListener != null) {
                    sListener.permissionSuccess();
                }
            } else {
                // 授权失败
                if (sListener != null) {
                    sListener.permissionFail();
                }
            }
            finish();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            sListener = null;
        }
    }
    

    注意:当通过Intent 跳转到未知应用授权列表的时候,一定要加上包名,这样就能直接跳转到你的app下,不然只能跳转到列表。


    跳转未知来源应用授权.png

    好了,这样Android 8.0 上也可以在线更新了。

    六、完整代码,封装了一个类AppDownloadManager

    为了不依赖于某个Activity ,因此封装了一个AppDownloadManager
    ,少量几行代码就可以实现在线更新,给出完整代码:

    public class AppDownloadManager {
        public static final String TAG = "AppDownloadManager";
        private WeakReference<Activity> weakReference;
        private DownloadManager mDownloadManager;
        private DownloadChangeObserver mDownLoadChangeObserver;
        private DownloadReceiver mDownloadReceiver;
        private long mReqId;
        private OnUpdateListener mUpdateListener;
    
        public AppDownloadManager(Activity activity) {
            weakReference = new WeakReference<Activity>(activity);
            mDownloadManager = (DownloadManager) weakReference.get().getSystemService(Context.DOWNLOAD_SERVICE);
            mDownLoadChangeObserver = new DownloadChangeObserver(new Handler());
            mDownloadReceiver = new DownloadReceiver();
        }
    
        public void setUpdateListener(OnUpdateListener mUpdateListener) {
            this.mUpdateListener = mUpdateListener;
        }
    
        public void downloadApk(String apkUrl, String title, String desc) {
            // fix bug : 装不了新版本,在下载之前应该删除已有文件
            File apkFile = new File(weakReference.get().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_name.apk");
    
            if (apkFile != null && apkFile.exists()) {
                apkFile.delete();
            }
    
            DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
            //设置title
            request.setTitle(title);
            // 设置描述
            request.setDescription(desc);
            // 完成后显示通知栏
            request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
            request.setDestinationInExternalFilesDir(weakReference.get(), Environment.DIRECTORY_DOWNLOADS, "app_name.apk");
            //在手机SD卡上创建一个download文件夹
            // Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).mkdir() ;
            //指定下载到SD卡的/download/my/目录下
            // request.setDestinationInExternalPublicDir("/codoon/","codoon_health.apk");
    
            request.setMimeType("application/vnd.android.package-archive");
            //
            mReqId = mDownloadManager.enqueue(request);
        }
    
        /**
         * 取消下载
         */
        public void cancel() {
            mDownloadManager.remove(mReqId);
        }
    
        /**
         * 对应 {@link Activity }
         */
        public void resume() {
            //设置监听Uri.parse("content://downloads/my_downloads")
            weakReference.get().getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true,
                    mDownLoadChangeObserver);
            // 注册广播,监听APK是否下载完成
            weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        }
    
        /**
         * 对应{@link Activity#onPause()} ()}
         */
        public void onPause() {
            weakReference.get().getContentResolver().unregisterContentObserver(mDownLoadChangeObserver);
            weakReference.get().unregisterReceiver(mDownloadReceiver);
        }
    
        private void updateView() {
            int[] bytesAndStatus = new int[]{0, 0, 0};
            DownloadManager.Query query = new DownloadManager.Query().setFilterById(mReqId);
            Cursor c = null;
            try {
                c = mDownloadManager.query(query);
                if (c != null && c.moveToFirst()) {
                    //已经下载的字节数
                    bytesAndStatus[0] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    //总需下载的字节数
                    bytesAndStatus[1] = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    //状态所在的列索引
                    bytesAndStatus[2] = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
                }
            } finally {
                if (c != null) {
                    c.close();
                }
            }
    
            if (mUpdateListener != null) {
                mUpdateListener.update(bytesAndStatus[0], bytesAndStatus[1]);
            }
    
            Log.i(TAG, "下载进度:" + bytesAndStatus[0] + "/" + bytesAndStatus[1] + "");
        }
    
        class DownloadChangeObserver extends ContentObserver {
    
            /**
             * Creates a content observer.
             *
             * @param handler The handler to run {@link #onChange} on, or null if none.
             */
            public DownloadChangeObserver(Handler handler) {
                super(handler);
            }
    
            @Override
            public void onChange(boolean selfChange) {
                super.onChange(selfChange);
                updateView();
            }
        }
    
        class DownloadReceiver extends BroadcastReceiver {
    
            @Override
            public void onReceive(final Context context, final Intent intent) {
                boolean haveInstallPermission;
                // 兼容Android 8.0
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    
                    //先获取是否有安装未知来源应用的权限
                    haveInstallPermission = context.getPackageManager().canRequestPackageInstalls();
                    if (!haveInstallPermission) {//没有权限
                        // 弹窗,并去设置页面授权
                        final AndroidOInstallPermissionListener listener = new AndroidOInstallPermissionListener() {
                            @Override
                            public void permissionSuccess() {
                                installApk(context, intent);
                            }
    
                            @Override
                            public void permissionFail() {
                                ToastUtils.shortToast(context, "授权失败,无法安装应用");
                            }
                        };
    
                        AndroidOPermissionActivity.sListener = listener;
                        Intent intent1 = new Intent(context, AndroidOPermissionActivity.class);
                        context.startActivity(intent1);
    
    
                    } else {
                        installApk(context, intent);
                    }
                } else {
                    installApk(context, intent);
                }
    
            }
        }
    
    
        /**
         * @param context
         * @param intent
         */
        private void installApk(Context context, Intent intent) {
            long completeDownLoadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
    
            Logger.e(TAG, "收到广播");
            Uri uri;
            Intent intentInstall = new Intent();
            intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intentInstall.setAction(Intent.ACTION_VIEW);
    
            if (completeDownLoadId == mReqId) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0以下
                    uri = mDownloadManager.getUriForDownloadedFile(completeDownLoadId);
                } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { // 6.0 - 7.0
                    File apkFile = queryDownloadedApk(context, completeDownLoadId);
                    uri = Uri.fromFile(apkFile);
                } else { // Android 7.0 以上
                    uri = FileProvider.getUriForFile(context,
                            "package_name.fileProvider",
                            new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_name.apk"));
                    intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                }
    
                // 安装应用
                Logger.e("zhouwei", "下载完成了");
    
                intentInstall.setDataAndType(uri, "application/vnd.android.package-archive");
                context.startActivity(intentInstall);
            }
        }
    
        //通过downLoadId查询下载的apk,解决6.0以后安装的问题
        public static File queryDownloadedApk(Context context, long downloadId) {
            File targetApkFile = null;
            DownloadManager downloader = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
    
            if (downloadId != -1) {
                DownloadManager.Query query = new DownloadManager.Query();
                query.setFilterById(downloadId);
                query.setFilterByStatus(DownloadManager.STATUS_SUCCESSFUL);
                Cursor cur = downloader.query(query);
                if (cur != null) {
                    if (cur.moveToFirst()) {
                        String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                        if (!TextUtils.isEmpty(uriString)) {
                            targetApkFile = new File(Uri.parse(uriString).getPath());
                        }
                    }
                    cur.close();
                }
            }
            return targetApkFile;
        }
    
        public interface OnUpdateListener {
            void update(int currentByte, int totalByte);
        }
    
        public interface AndroidOInstallPermissionListener {
            void permissionSuccess();
    
            void permissionFail();
        }
    }
    

    使用就很简单了,如下:

    (1) 弹出更新提示框:提示用户更新

     private void showUpdateDialog(final AppUpdateInfo updateInfo) {
            AppUpdateDialog dialog = new AppUpdateDialog(getContext());
            dialog.setAppUpdateInfo(updateInfo);
            dialog.setOnUpdateClickListener(new AppUpdateDialog.OnUpdateClickListener() {
                @Override
                public void update(final AppUpdateDialog updateDialog) {
                    String title = "app name";
                    String desc = "版本更新";
    
                    mDownloadManager.setUpdateListener(new AppDownloadManager.OnUpdateListener() {
                        @Override
                        public void update(int currentByte, int totalByte) {
                            updateDialog.setProgress(currentByte, totalByte);
                            if ((currentByte == totalByte) && totalByte != 0) {
                                updateDialog.dismiss();
                            }
    
                        }
                    });
    
                    mDownloadManager.downloadApk(updateInfo.download_url, title, desc);
                }
    
            });
            dialog.setCanceledOnTouchOutside(false);
            dialog.setCancelable(false);
            dialog.show();
    
        }
    

    (2) 注意在 onResume 和 onPause 调用对应方法:

     @Override
        public void onResume() {
            super.onResume();
            if (mDownloadManager != null) {
                mDownloadManager.resume();
            }
    }
    
     @Override
        public void onPause() {
            super.onPause();
            if (mDownloadManager != null) {
                mDownloadManager.onPause();
            }
    
        }
    
    app更新.png

    七、总结

    本文总结了项目中app在线更新遇到的一些适配问题,关于Android 6.0 的适配,如果你没有使用DownloadManager,可能不会遇到这个问题。7.0 和 8.0 的适配不管用哪种方式,都会有。

    关于app在线更新版本适配就此结束,如果有啥问题,欢迎指出。

    相关文章

      网友评论

      • 花儿一样的:AppUpdateDialog 这个代码能贴出来看看么?
        依然范特稀西:@花儿一样的 更新在文末了
      • 影子_2204:完美! 特地登录来感谢你的:+1:
        依然范特稀西:@影子_2204 😄😄
      • 我的橘子分你一瓣:一加6手机 Android 8.0 调用requestPermissions(new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, RC_INSTALL_PACKAGES);不弹窗?????
        Li_sten:@阳光大宅男y 那该怎么弄?我也碰到了这个问题
        阳光大宅男y:一加手机系统里有自己的权限管理,所以requestPermissions是不管用的。
      • 6c5e423cdea8:可以的
      • 特色吧:9.0没有适配
      • CLOWN_c0df:下载完成后没有跳出安装的对话框
      • CLOWN_c0df:// 注册广播,监听APK是否下载完成
        weakReference.get().registerReceiver(mDownloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        注册广播出错了
      • CLOWN_c0df:AndroidOPermissionActivity这个是什么
        依然范特稀西:@花儿一样的 ??
        花儿一样的:8.0权限申请提示页面
      • 灬秋_1bef:有源码吗 参考一下谢谢
        灬秋_1bef:@依然范特稀西 那个updatadialog可以发给我一下吗
        依然范特稀西:@灬秋_1bef 源码都在文章里了
      • flemingchen:设置监听Uri.parse("content://downloads/my_downloads"),这里的路径是要和provider_paths.xml中的Download一样吗
        flemingchen:@依然范特稀西 那都是content://downloads/my_downloads吗?
        依然范特稀西:不需要,固定的
      • 12067969ae74:感谢大佬分享,有技术问题,可以问吗
      • b12fcf9f17c1:5.0 6.0都没问题,7.0和8.0报解析包错误
      • 从心开始的我:以前想用这个更新的 结果华为手机一堆毛病 无奈换成FileDownloader。。。
      • 山顶蚂蚁:有些手机压根没有下载功能 GG
        依然范特稀西:@山顶蚂蚁 一首凉凉送给自己
      • 有点健忘:那个是不是target 23以上才会有问题啊,以前一直用这个类下载,没注意过要适配这么多,收藏了,万一以后碰到了。现在新公司下载是别人写的插件,咋也不知道咋弄的。
        Wendy_16ce:@有点健忘 不错
        依然范特稀西:@有点健忘 稳👍
      • fffb4eaf11c2:楼主, 8.0 下无法监听下载进度呢,而且也没有下载完成回调,试过 7.0、5.1的都没问题
        依然范特稀西:@温暖的铁匠 没有这个机子没有测,我手上只有一个一加手机是8.0系统,测了没问题
        fffb4eaf11c2:@依然范特稀西 真机啊 LG Nexus 5X
        依然范特稀西:@温暖的铁匠 可以啊,你用的真机还是模拟器?
      • Go_oG:不知道作者遇到在某些华为机型 Android版本为4.X时DownloadManager下载下来的文件变成了zip格式的这个问题没有
        依然范特稀西:@Go_oG 没有遇到啊,手里有个华为4.4 ,是正常的
      • 勤息嘻嘻嘻:感谢大佬分享~
        Ever_6822:DownloadManager很多机型会下载不了,推荐一篇我的博文——使用Retrofit2+RxJava2实现Android App自动更新 https://blog.csdn.net/ever69/article/details/80222543
      • a6393228bea9:请问作者有没有源码demo可以供参考
        依然范特稀西:@卡布瑞克 就是一个简单的自定义Dialog
        卡布瑞克:@依然范特稀西 能更新 AppUpdateDialog 部分的源码吗,不胜感激~
        依然范特稀西:@分享杰 源码差不多都在这儿了
      • OblivionU:赠人玫瑰,手留余香。感谢分享!再接再厉!请问有没有方便添加的QQ或者微信呢
        依然范特稀西:@OblivionU 关注我的微信公众号:Android 技术杂货铺,上面有微信qq群联系方式

      本文标题:Android app 在线更新那点事儿(适配Android6.

      本文链接:https://www.haomeiwen.com/subject/winucftx.html