安卓应用开发版本更新

访客 198 0

安卓开发实战之app之版本更新升级(DownloadManager和http下载)完整实现

 转载

博主文章分类:14 其他随笔 ©著作权 wx610a246613cb02021-08-05 17:02:56

文章标签安卓实战开发androidide服务器版本更新文章分类Android移动开发阅读数1605

前言

本文将介绍关于App升级与更新的内容。通常情况下,用户可以通过以下两种方式获取升级提醒:

  • 一种是通过应用市场 获取
  • 一种是打开应用之后提醒用户更新升级

用户点击升级按钮后,更新操作通常开始执行。升级操作可以分为两种形式:

  • 一般升级
  • 强制升级

如何进行app升级操作:

  • 应用市场的app升级

在App Store中进行升级需要上传新版的App。一旦我们完成了新版本的开发,就会将其上传至App Store。经过审核后,即可正式发布于应用市场并上线。此时,用户若已安装该应用市场,则会收到有关我们App新版本升级的提醒通知。

  • 应用内升级

除了可以在应用市场升级,我们还可以在应用内升级,在应用内升级主要是通过调用服务器端接口获取应用的升级信息,然后通过获取的服务器升级应用信息与本地的App版本比对,若服务器下发的最新的App版本高于本地的版本号,则说明有新版本发布,那么我们就可以执行更新操作了,否则忽略掉即可。

很明显,我们并不把应用市场提醒的升级作为重点。本文主要关注于实现不同角度的app升级场景,以便在未来的开发过程中直接使用。

服务器端:

  • 服务端提供一个接口,或者网址,这里提供一个网址如下:
作为一名安卓程序员,通常在测试时需要编写一个服务器端(真是麻烦),我会使用nodejs来创建一个本地服务器,以便测试应用的版本更新验证。这个本地服务器的地址是:http://192.168.191.1:8081/update
  • 根据请求的结果,我这里就写一个简单的json
{ data: { appname: hoolay.apk, serverVersion: 1.0.2, serverFlag: 1,  lastForce : 1,  updateurl: http://releases.b0.upaiyun.com/hoolay.apk,  
```javascript
const express = require('express');
const app = express();
const fs = require(fs);

app.get('/update', function (req, res) {
    fs.readFile(__dirname + /version.json, 'utf8', function (err, data) {
        console.log(data);
        res.end(data);
    });
});

var server = app.listen(8081);
```
 
  • 效果如下:

安卓应用开发版本更新-第1张图片-谷歌商店上架

请参考下图,显示的是打开浏览器后的结果。

安卓应用开发版本更新-第2张图片-谷歌商店上架

上图展示了WebStorm终端的输出结果。

客户端需要实现:

我们了解到不同的需求会有不同的操作方式和界面展示。

  1. 关于下载方式,无论是在应用内部下载还是通过通知栏更新:

    • app内下载更新

    在下载和安装完成之后,我们才能进行操作。结果如下:

    安卓应用开发版本更新-第3张图片-谷歌商店上架

    • 通知栏下载更新

    在这种情况下,应用内不会进行更新,而是将其放置在通知栏中,这样不会对当前app的使用产生影响。具体效果如下:

    安卓应用开发版本更新-第4张图片-谷歌商店上架

  2. 应用程序的更新可以分为三种类型:强制更新、推荐更新和无需更新。

    • 强制更新

      安卓应用开发版本更新-第5张图片-谷歌商店上架

    • 推荐更新

      安卓应用开发版本更新-第6张图片-谷歌商店上架

    • 无需更新

      安卓应用开发版本更新-第7张图片-谷歌商店上架

具体方案:

  1. 实现bean用于对接后端接口实现app的更新(不写网络请求模拟本地数据也需要这个模型)
  2. 使用retrofit来请求版本更新接口
  3. 下载apk我们分别使用DownloadManager和普通的httpurlconnection
  4. 通过BroadcastReceiver来监听是否下载完成

Bean preparation

首先,我们需要对服务端提供的JSON进行解析。为此,我们需要创建一个Bean类来严格按照JSON文件的格式进行定义。

```java
package com.losileeya.appupdate.bean;

/**
 * User: Losileeya ([email protected])
 * Date: 2016-09-27
 * Time: 11:20
 * 类描述:版本更新的实体与你服务器的字段相匹配
 * @version :
 */
public class UpdateAppInfo {
    public UpdateInfo data; // 信息
    public Integer error_code; // 错误代码
    public String error_msg; // 错误信息

    public static class UpdateInfo{
        // app名字        
        public String appname;
        //服务器版本        
        public String serverVersion;
        //服务器标志        
        public String serverFlag;
        //强制升级        
        public String lastForce;
      	//app最新...
```

改为:

```java
package com.losileeya.appupdate.bean;

/**
 * User: Losileeya ([email protected])
 * Date: 2016-09-27
 * Time: 11:20 AM 
 *
 * Description:
 *
 */

public class UpdateAppInfo {
    
    /**
     * Information about the update.
     */
    public UpdateInfo data;

  	/**
  		Error code.
  	*/
  	public Integer error_code;

  	/**
  		Error message.
  	*/
  	public String error_msg;


	  /**
	  	Update information for the app.
	  */
	 	public static class UpdateInfo {

	  		/**
	  			App name.
	  		*/
	  		public String appname;

		  	
		  	
		  	
		    /**
		    	Server version of the app.  
		    */
		   	public String serverVersion;


		    
		    
		    /**
		    	Server flag of the app.  
		    */
	    	public String serverFlag;



	    	
	    	
	      /**
	      	Latest force upgrade for the app.  
	      */
	      public String lastForce;

	    	
	    	
		    /**
		    	Latest version of the app.
		    */
		    ...
```   

我在这里运用retrofit和rxjava进行练笔。

先加入 依赖

compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
compile 'io.reactivex:rxjava:1.1.0' // 推荐同时加载RxJava
compile 'com.squareup.retrofit:retrofit:2.0.0-beta2' // Retrofit网络处理
compile 'com.squareup.retro... 
public interface ApiService {
    // 实际开发过程中可能的接口方式
    @GET(update)
    Observable getUpdateInfo(
        @Query(appname) String appname,
        @Query(serverVersion) String appVersion
    );
} 
public class ServiceFactory {
    private static final String BASEURL = http://192.168.191.1:8081/;
    
    public static  T createServiceFrom(final Class serviceClass) {
        Retrofit adapter = new Retrofit.Builder()
                .baseUrl(BASEURL)
                .addCallAdapterFactory(RxJavaCallAdapter
 
@SuppressWarnings(unused)
public static void checkUpdate(String appCode, String curVersion, final CheckCallBack updateCallback) {
    ApiService apiService = ServiceFactory.createServiceFrom(ApiService.class);
    apiService.getUpdateInfo()
        // .apiService.getUpdateInfo(appCode, curVersion)
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber() {
            @Override
            public void onCompleted() {
            }
            
            @Override
            public void onError(Throwable e) {
            }
            
            @Override
            public void onNext(UpdateAppInfo updateAppInfo) {
                // TODO: Implement the logic for handling the update info
                // You can use the 'updateCallback' to notify the caller about the update information.
                // For example: 
                // if (updateCallback != null) {
                //     updateCallback.onCheckComplete(updateAppInfo);
                //}
                
               
           }
       });
} 

请提供结果回调监听。

```java
public interface CheckCallBack {
    // 检测成功或失败的相关接口
    void onSuccess(UpdateAppInfo updateInfo);
    void onError();
}
```

具体使用接口的处理:
1. 
2. 
3. 
4. 
// 检查网络是否需要更新版本
CheckUpdateUtils.checkUpdate(apk, 1.0.0, new CheckUpdateUtils.CheckCallBack() {
    @Override
    public void onSuccess(UpdateAppInfo updateInfo) {
        String isForce = updateInfo.data.getLastForce(); // 是否需要强制更新
        String downUrl = updateInfo.data.getUpdateurl(); // apk下载地址
        String updateinfo = updateInfo.data.getUpgradeinfo(); // apk更新详情
        String appName = updateInfo.data.getAppname();
        
        if (isForce.equals(1) && !TextUtils.isEmpty(updateinfo)) { // 强制更新

 
UpdateAppInfo.UpdateInfo info = new UpdateAppInfo.UpdateInfo();
info.setLastForce(1);
info.setAppname(我日你);
info.setUpgradeinfo(whejjefjhrherkjreghgrjrgjjhrh);
info.setUpdateurl(http://releases.b0.upaiyun.com/hoolay.apk);

if(info.getLastForce().equals(1)){//强制更新
    forceUpdate(); 
private void forceUpdate(final Context context, final String appName, final String downUrl, final String updateinfo) {
    mDialog = new AlertDialog.Builder(context);
    mDialog.setTitle(appName + 有新的更新!);
    mDialog.setMessage(updateinfo);
    mDialog.setPositiveButton(立即更新, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            if (!canDownloadState()) {
                showDownloadSettings();
            }
        }
    });
}   
改写后的文案如下:

```java
import androidx.appcompat.app.AlertDialog;
```

然后显然是不能按返回键取消的,我们需要   
使用谷歌推荐的DownloadManager实现下载,设置为不可取消。 

在api level 9之后,我们可以使用Android自带的DownloadManager模块进行下载。通过通知栏,我们可以了解到该模块是系统自带的,并且它已经为我们处理了下载失败、重新下载等功能。因此,整个下载过程完全由系统负责,无需我们过多干预。

DownLoadManager.Query是一个用于查询下载信息的主要功能。

DownLoadManager.Request是一个用于发起下载请求的主要工具。

首先,让我们来看一下简单的实现:

以下是创建Request对象的代码:

DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkurl));
// 设置下载仅在Wi-Fi网络下进行
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
// 设置通知栏可见
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setTitle(下载);
request.setDescription(apk正在下载);
request.setAllowe 
下面是关于request属性的一些信息,你可以参考:

1. 通过以下代码获取DownloadManager的实例:
```java
DownloadManager downManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
```

2. 使用以下代码将请求添加到下载队列中,并获取其ID:
```java
long id = downManager.enqueue(request);
```

希望对你有所帮助! 
addRequestHeader(String header, String value): 用于添加网络下载请求的HTTP头信息。
allowScanningByMediaScanner(): 用于设置是否允许本MediaScanner扫描。
setAllowedNetworkTypes(int flags): 设置下载时可使用的网络类型,默认为任何网络都可以下载。提供的网络常量有:NETWORK_BLUETOOTH、NETWORK_MOBILE、NETWORK_WIFI。
setAllowedOverRoaming(Boolean allowed): 设置漫游状态下是否可以下载。
setNotificationVisibility(int visibility): 设置下载时在状态栏显示通知信息的可见性。
setTitle(CharSequence title): 用于设置标题。   
  1. 我们使用downloaderManager来下载apk文件,并将downManager.enqueue(request)返回的id值保存在本地。通过这个id,我们可以获取apk文件的下载路径和下载状态,并根据状态更新通知栏显示。

  2. 当您首次成功下载时,将会弹出一个安装界面。

    如果用户在未点击安装的情况下按下返回键,然后在某个时间点再次使用我们的APP。

    当成功下载后,需验证本地apk的包名是否与当前程序相同,并且本地apk的版本号必须高于当前程序版本。若以上条件均满足,则直接启动安装程序。

代码实现的具体细节如下:

实现文件下载管理的方法包括创建请求并将其添加到下载队列中,然后通过返回的ID来获取下载路径和下载状态。

public class FileDownloadManager {
    private DownloadManager downloadManager;
    private Context context;
    private static FileDownloadManager instance;

    private FileDownloadManager(Context context) {
        downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
        this.context = context.getApplicationContext();
    }

    public static FileDownloadManager getInstance(Context context) {
        if (instance == null) {
            instance = new FileDownloadManager(context);
        }
        return instance;
    }

    /**
     * @param uri
     * @param title
     * @param description
     * @return download id
     */
    public long startDownload(String uri, String title, String description, String appName) {
        DownloadManager.Request req = new DownloadManager.Request(Uri.parse(uri));
        req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI);
        
         //req.setAllowedOverRoaming(false); 
         req.setNotificationVisibility(Downloa 
public class DownLoadApk {
    public static final String TAG = DownLoadApk.class.getSimpleName();

    public static void download(Context context, String url, String title, final String appName) {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        long downloadId = sp.getLong(DownloadManager.EXTRA_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.getDownloadManager().remove(downloadId);
                    }
                }

                start(context, url, title, appName);
            } else if (status == DownloadManager.STATUS_FAILED) {
                start(context, url, title ,appName);
            } else { 
               Log.d(TAG,apk is already downloading);
           }
       } else { 
           start(context,url,title ,appName); 
       }
   }

   private static void start(Context context,String url,String title,String appName){
       long id=FileDownloadManager.getInstance(context).startDownload(url,
               title,下载完成后点击打开,appName);

       SharedPreferences sp=PreferenceManager.getDefaultSharedPreferences
               (context);sp.edit().putLong(DownloadManage 

监听app是否安装完成

public class ApkInstallReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {
            long downloadApkId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
            installApk(context, downloadApkId);
        }
    }

    /**
     * 安装apk
     */
    private void installApk(Context context, long downloadApkId) {
        // 获取存储ID
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        long downId = sp.getLong(DownloadManager.EXTRA_DOWNLOAD_ID,-1L);
        
        if (downloadApkId == downId) {
            DownloadManager downManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
            Uri downloadFileUri = downManager.getUriForDownloadedFile(downloadApkId);

 

配置清单:

请先授予网络下载的权限:

 <uses-permission android:name="android.permission.INTERNET"/>    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  • 1.
  • 2.

另外,还需要添加以下静态广播权限:<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>= Build.VERSION_CODES.N) { Uri uriForFile = FileProvider.getUriForFile(context, context.getPackageName() + .fileprovider, apkFile); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.setDataAndType(uriForFile, application/vnd.android.package-archive); } else { Uri uriFromFile=Uri.fromFile(apkPath);//Android 7.0以下版本使用此方法获取URI intent.setDataAndType(uriFromFile,application/vnd.android.package-archive); } context.startActivity(intent); } }

因此,在使用该组件之前,需要进行可用性判断:

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.COMPO 
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); 

概述DownloadManager的使用方法:

  1. 构建下载请求:

    修改后的文案如下:
    
    ```java
    DownloadManager.Request request = new DownloadManager.Request(url);
    request.setXXX(); // 设置请求属性
    ```
    
    或者
    
    ```java
    DownloadManager.Request request = new DownloadManager.Request(url);
    request.setXXX(); // 设置请求属性
    ```
    
    请注意,上述代码中的`setXXX()`应该替换为具体的方法名,以设置相应的请求属性。 
    调用`request.setXXX()`方法,然后使用downloadmanager对象的enqueue方法进行下载。该方法返回一个编号,用于标识此下载任务。 
    获取下载管理器实例,并将下载请求加入到下载队列中:
    
    ```java
    DownloadManager downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
    long downloadId = downloadManager.enqueue(request);
    ```
    
    为了对当前的所有任务进行保存和管理,我们可以使用 DownloadManager.Query 对象来获取这些信息。通过该对象,我们可以查询所有的下载任务信息。 

    使用setFilterById方法,可以根据任务编号来查询下载任务的信息。

    setFilterByStatus(int flags):根据下载状态查询下载任务

  2. 如果想取消下载,则可以调用remove方法完成,此方法可以将下载任务和已经下载的文件同时删除:

    下面是修改后的文案:
    
    ```java
    downManager.remove(id);
    ```
    
    好了,具体的都讲得差不多了。本文以同步到我的 GitHub。 

    示例传送门:AppUpdate.rar

    后记

    鉴于版本更新没有对6.0的权限和7.0的FileProvider做适配,导致6.0和7.0的安装失败或者7.0直接crash,本文将不再解释,自行处理这里提供一个已经适配的demo下载 

标签: 版本 谷歌推荐 安卓实战开发 安卓开发实战

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~