在Android App都会有版本更新的功能,以前我们公司是用友盟SDK更新功能,自己服务器没有这样的功能。版本检测、Apk下载都是使用友盟。最近看到友盟的版本更新SDK文档:十月份更新功能将会停止服务器,建议开发者迁移到自己的服务器中。

本文章的主要逻辑:

第一次下载成功,弹出安装界面;

如果用户没有点击安装,而是按了返回键,在某个时候,又再次使用了我们的APP

如果下载成功,则判断本地的apk的包名是否和当前程序是相同的,并且本地apk的版本号大于当前程序的版本,如果都满足则直接启动安装程序。

下载功能,Google官方推荐使用 DownloadManager 服务。

1. 如何使用DownloadManager

FileDownloadManager.java

    public long startDownload(String uri, String title, String description) {
        DownloadManager.Request req = new DownloadManager.Request(Uri.parse(uri));

        req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
        //req.setAllowedOverRoaming(false);

        req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);

        //设置文件的保存的位置[三种方式]
        //第一种
        //file:///storage/emulated/0/Android/data/your-package/files/Download/update.apk
        req.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "update.apk");
        //第二种
        //file:///storage/emulated/0/Download/update.apk
        //req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "update.apk");
        //第三种 自定义文件路径
        //req.setDestinationUri()

        // 设置一些基本显示信息
        req.setTitle(title);
        req.setDescription(description);
        req.setMimeType("application/vnd.android.package-archive");

        //加入下载队列
        return dm.enqueue(req);

        //long downloadId = dm.enqueue(req);
        //Log.d("DownloadManager", downloadId + "");
        //dm.openDownloadedFile()
    }

Android自带的DownloadManager模块来下载, 我们通过通知栏知道, 该模块属于系统自带, 它已经帮我们处理了下载失败、重新下载等功能。

2. 如何检测下载完成,然后启动安装界面

DownloadManager下载完成后会发出一个广播 android.intent.action.DOWNLOAD_COMPLETE 新建一个广播接收者即可:

public class ApkInstallReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
            // @TODO SOMETHING
        }
    }
}

3. 完善App更新逻辑

1> 第一次下载成功,弹出安装界面

这个逻辑在ApkInstallReceiver里做即可:


public class ApkInstallReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
           installApk(context, downloadApkId);
        }
    }

    private static void installApk(Context context, long downloadApkId) {
        DownloadManager dManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        Intent install = new Intent(Intent.ACTION_VIEW);
        Uri downloadFileUri = dManager.getUriForDownloadedFile(downloadApkId);
        if (downloadFileUri != null) {
            Log.d("DownloadManager", downloadFileUri.toString());
            install.setDataAndType(downloadFileUri, "application/vnd.android.package-archive");
            install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(install);
        } else {
            Log.e("DownloadManager", "download error");
        }
    }
}

2> 如果用户没有点击安装,而是按了返回键

这个时候用户可能没有安装,而是退出了安装界面,用户退出了我们的APP,在某个时候,又再次使用了我们的APP,这个时候我们不应该去下载新版本,而是使用已经下载已经存在本地的APK。

第一次下载的 downloadManager.enqueue(req)会返回一个downloadId,把downloadId保存到本地,用户下次进来的时候,取出保存的downloadId,然后通过downloadId来获取下载的状态信息。

ApkUpdateUtils.java

public static void download(Context context, String url, String title) {
        long downloadId = SpUtils.getInstance(context).getLong(KEY_DOWNLOAD_ID, -1L);
        if (downloadId != -1L) {
            FileDownloadManager fdm = FileDownloadManager.getInstance(context);
            int status = fdm.getDownloadStatus(downloadId);
            if (status == DownloadManager.STATUS_SUCCESSFUL) {
                //启动更新界面
                Uri uri = fdm.getDownloadUri(downloadId);
                if (uri != null) {
                    if (compare(getApkInfo(context, uri.getPath()), context)) {
                        startInstall(context, uri);
                        return;
                    } else {
                        fdm.getDm().remove(downloadId);
                    }
                }
                start(context, url, title);
            } else if (status == DownloadManager.STATUS_FAILED) {
                start(context, url, title);
            } else {
                if (Config.DEV_MODE) {
                    Log.d(TAG, "apk is already downloading");
                }
            }
        } else {
            start(context, url, title);
        }
    }

获取APK包名、版本号

    /**
     * 获取apk程序信息[packageName,versionName...]
     *
     * @param context Context
     * @param path    apk path
     */
    private static PackageInfo getApkInfo(Context context, String path) {
        if (Config.DEV_MODE) {
            Log.d(TAG, "apk download path: " + path);
        }
        PackageManager pm = context.getPackageManager();
        PackageInfo info = pm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
        if (info != null) {
            //String packageName = info.packageName;
            //String version = info.versionName;
            //Log.d(TAG, "packageName:" + packageName + ";version:" + version);
            //String appName = pm.getApplicationLabel(appInfo).toString();
            //Drawable icon = pm.getApplicationIcon(appInfo);//得到图标信息
            return info;
        }
        return null;
    }

获取下载完成的APK地址

    /**
     * 获取保存文件的地址
     *
     * @param downloadId an ID for the download, unique across the system.
     *                   This ID is used to make future calls related to this download.
     * @see FileDownloadManager#getDownloadPath(long)
     */
    public Uri getDownloadUri(long downloadId) {
        return dm.getUriForDownloadedFile(downloadId);
    }

    public DownloadManager getDm() {
        return dm;
    }

获取下载状态信息

    public int getDownloadStatus(long downloadId) {
        DownloadManager.Query query = new DownloadManager.Query().setFilterById(downloadId);
        Cursor c = dm.query(query);
        if (c != null) {
            try {
                if (c.moveToFirst()) {
                    return c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));

                }
            } finally {
                c.close();
            }
        }
        return -1;
    }

如果下载失败,则重新下载并且把downloadId存起来。

    private static void start(Context context, String url, String title) {
        long id = FileDownloadManager.getInstance(context).startDownload(url,
                title, "下载完成后点击打开");
        SpUtils.getInstance(context).putLong(KEY_DOWNLOAD_ID, id);
        if (Config.DEV_MODE) {
            Log.d(TAG, "apk start download " + id);
        }
    }

如果下载成功,则判断本地的apk的包名是否和当前程序是相同的,并且本地apk的版本号大于当前程序的版本,如果都满足则直接启动安装程序。

    /**
     * 下载的apk和当前程序版本比较
     *
     * @param apkInfo apk file's packageInfo
     * @param context Context
     * @return 如果当前应用版本小于apk的版本则返回true
     */
    private static boolean compare(PackageInfo apkInfo, Context context) {
        if (apkInfo == null) {
            return false;
        }
        String localPackage = context.getPackageName();
        if (apkInfo.packageName.equals(localPackage)) {
            try {
                PackageInfo packageInfo = context.getPackageManager().getPackageInfo(localPackage, 0);
                if (apkInfo.versionCode > packageInfo.versionCode) {
                    return true;
                } else {
                    if (Config.DEV_MODE) {
                        Log.d(TAG, "apk's versionCode <= app's versionCode");
                    }
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
        if (Config.DEV_MODE) {
            Log.d(TAG, "apk's package not match app's package");
        }
        return false;
    }

启动安装界面

    public static void startInstall(Context context, Uri uri) {
        Intent install = new Intent(Intent.ACTION_VIEW);
        install.setDataAndType(uri, "application/vnd.android.package-archive");
        install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(install);
    }

为了严谨起见,在ApkInstallReceiver里不仅要对downloadId判断,还应当把当前程序和本地apk包名和版本号对比。

如果用户禁用了下载服务[下载管理程序]

可以通过如下代码进入 启用/禁用 下载管理程序 界面:

    String packageName = "com.android.providers.downloads";
    Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + packageName));
    startActivity(intent);

我们先停止下载管理程序,然后点击demo里的Download 按钮报出如下错误:

Caused by: java.lang.IllegalArgumentException: Unknown URL content://downloads/my_downloads
          at android.content.ContentResolver.insert(ContentResolver.java:1227)
          at android.app.DownloadManager.enqueue(DownloadManager.java:946)
          at com.chiclam.download.FileDownloadManager.startDownload(FileDownloadManager.java:61)
          at com.chiclam.download.ApkUpdateUtils.start(ApkUpdateUtils.java:47)
          at com.chiclam.download.ApkUpdateUtils.download(ApkUpdateUtils.java:42)
          at com.chiclam.download.MainActivity.download(MainActivity.java:34)
          at java.lang.reflect.Method.invoke(Native Method)
          at android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:288)
          at android.view.View.performClick(View.java:5204)
          at android.view.View$PerformClick.run(View.java:21153)
          at android.os.Handler.handleCallback(Handler.java:739)
          at android.os.Handler.dispatchMessage(Handler.java:95)
          at android.os.Looper.loop(Looper.java:148)
          at android.app.ActivityThread.main(ActivityThread.java:5417)
          at java.lang.reflect.Method.invoke(Native Method)
          at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
          at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 

也就是说如果停止了下载管理程序 调用dm.enqueue(req);就会上面的错误,从而程序闪退.

所以在使用该组件的时候,需要判断该组件是否可用:

    private boolean canDownloadState() {
        try {
            int state = this.getPackageManager().getApplicationEnabledSetting("com.android.providers.downloads");

            if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
                    || state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
                return false;
            }

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

具体详情, 查看代码.

完整源码下载:https://github.com/chiclaim/android-app-update

Android 使用DownloadManager进行版本更新的完整方案的更多相关文章

  1. Android:使用 DownloadManager 进行版本更新,出现 No Activity found to handle Intent 及解决办法

    项目中,进行版本更新的时候,用的是自己写的下载方案,最近看到了使用系统服务 DownloadManager 进行版本更新,自己也试试. 在下载完成以后,安装更新的时候,出现了一个 crash,抓取的 ...

  2. Android:使用 DownloadManager 进行版本更新

    app 以前的版本更新使用的自己写的代码从服务器下载,结果出现了下载完成以后,提示解析包错误的问题,但是呢,找到该 apk 点击安装是可以安装成功的,估计就是最后几秒安装包没有下载完成然后点击了安装出 ...

  3. Android 高清加载巨图方案 拒绝压缩图片

    Android 高清加载巨图方案 拒绝压缩图片 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/49300989: 本文出自:[张 ...

  4. Android 基于Netty的消息推送方案之对象的传递(四)

    在上一篇文章中<Android 基于Netty的消息推送方案之字符串的接收和发送(三)>我们介绍了Netty的字符串传递,我们知道了Netty的消息传递都是基于流,通过ChannelBuf ...

  5. Android 基于Netty的消息推送方案之字符串的接收和发送(三)

    在上一篇文章中<Android 基于Netty的消息推送方案之概念和工作原理(二)> ,我们介绍过一些关于Netty的概念和工作原理的内容,今天我们先来介绍一个叫做ChannelBuffe ...

  6. Android 基于Netty的消息推送方案之概念和工作原理(二)

    上一篇文章中我讲述了关于消息推送的方案以及一个基于Netty实现的一个简单的Hello World,为了更好的理解Hello World中的代码,今天我来讲解一下关于Netty中一些概念和工作原理的内 ...

  7. Android架构设计和软硬整合完整训练

    Android架构设计和软硬整合完整训练 Android架构设计和软硬整合完整训练:HAL&Framework&Native Service&Android Service&a ...

  8. Android手游《》斗地主完整的源代码(支持单机和网络对战)

    Android手游<斗地主>完整的源代码(支持单机和网络对战)下载.一个很不错的源代码. 斗地主掌游是一个独特的国内社会斗地主棋牌游戏,之后玩家可以下载网上斗地主和全世界.掌游斗地主特点: ...

  9. android SDK与ADT版本更新问题

    android SDK与ADT版本更新问题 问题:This Android SDK requires Android Developer Toolkit version 14.0.0 or above ...

随机推荐

  1. 机器学习技法:13 Deep Learning

    Roadmap Deep Neural Network Autoencoder Denoising Autoencoder Principal Component Analysis Summary

  2. 机器学习技法:05 Kernel Logistic Regression

    Roadmap Soft-Margin SVM as Regularized Model SVM versus Logistic Regression SVM for Soft Binary Clas ...

  3. 实验吧_拐弯抹角(url伪静态)&Forms

    拐弯抹角 先贴代码 <?php // code by SEC@USTC echo '<html><head><meta http-equiv="chars ...

  4. 推送本地项目至Github遇到的问题以及解决办法记录

    在把本地新项目推送至GitHub仓库时的大致流程和步骤,首先现在GitHub上面新建一个项目,复制该项目的 带.git 后缀的地址,比如 git@github.com:XXX/XXX.git 然后在本 ...

  5. [UOJ 41]【清华集训2014】矩阵变换

    Description 给出一个 $N$ 行 $M$ 列的矩阵A, 保证满足以下性质: $M > N$. 矩阵中每个数都是 $[0, N]$ 中的自然数. 每行中, $[1, N]$ 中每个自然 ...

  6. BZOJ3622 已经没有什么好害怕的了

    Description Input Output Sample Input 4 2 5 35 15 45 40 20 10 30 Sample Output 4 HINT 输入的2*n个数字保证全不相 ...

  7. 【Gcd】

    [题目描述] 有 n 个正整数 x1~xn,初始时状态均为未选.有 m 个操作,每个操作给定一个编号 i,将 xi 的选取状态取反.每次操作后,你需要求出选取的数中有多少个互质的无序数对. [输入数据 ...

  8. [bzoj1566][NOI2009]管道取珠

    来自FallDream的博客,未经允许,请勿转载,谢谢. n<=500 神题...... 发现这个平方可以看作两个序列相同的对数  然后就可以表示状态了. f[i][j][k]表示两个序列各选了 ...

  9. 音频自动增益 与 静音检测 算法 附完整C代码

    前面分享过一个算法<音频增益响度分析 ReplayGain 附完整C代码示例> 主要用于评估一定长度音频的音量强度, 而分析之后,很多类似的需求,肯定是做音频增益,提高音量诸如此类做法. ...

  10. 利用maven install jar到项目当中

    接着上面利用maven打好的jar包.把刚刚打好的包放入其他项目当中怎么办? 只需要在相同的目录下执行mvn install,maven会自动把jar放到本地仓库中. 这样,原先maven项目中缺少依 ...