項目需求討論-Android App升級

公司開發時候,應該最常用的就是APP升級功能,倒不是說的是熱修復等技術,而是普通的檢測到服務器版本比本地手機版本高的時候,手機會詢問用戶是否要下載最新的app,然后下載apk下來,然后進行安裝,我以前用的都是別人封裝好的。也沒仔細看過,這次又正好有這個需求,就老老實實自己寫了一下。

(PS:也可以用第三方公司出的,比如騰訊的Bugly等,也挺方便的,不過apk要上傳到Bugly的平臺上,但是有些公司會要求在自己平臺上,這時候這些第三方就無法使用了。)


-------------------------------------我是分割分割君---------------------------------

大家都知道應用升級,也都體驗過應用升級,而開發步驟也一般分為這么幾步(如果圖片里面缺少啥步驟,歡迎指出。):

我們就按照一步步來分析:

  1. 從服務器上獲取版本信息,怎么做呢,只要和你們后臺開發人員搞好關系即可。哈哈。一般需要他們提供這幾個字段。
 {   
        "versionCode": "1", 
        "versionName": "1.0", 
        "apkUrl": "http://java.linuxlearn.net/shelwee/Finances.apk",
        "updateTitle": "更新提示" ,
        "changeLog":"1.修復xxx Bug;\n2.更新UI界面."
    }
  1. 獲取本地APP的versionCode
public static int getPackageVersionCode(Context context){
        PackageManager manager = context.getPackageManager();
        PackageInfo packageInfo = null;
        try {
            packageInfo = manager.getPackageInfo(context.getPackageName(),0);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        if(packageInfo != null){
            return packageInfo.versionCode;
        }else{
            return 1;
        }
    }

然后和服務器那邊傳過來的versionCode字段進行比較,如果比我們本地獲取的APP的versionCode 大。那就進行下一步

3.我們也看到了,這里我分成了Android6.0為分割線做區別。因為Android6.0開始后,單純的在AndroidManifest.xml中定義權限已經不夠了。需要再代碼中動態讓用戶來確定才能給APP相應的權限。所以我們APP在AndroidManifest.xml中還是定義

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

因為在Android6.0系統下,就等于獲取到了這二者的權限。(下載APK當然要網絡權限和把文件寫入存儲的權限)

那如果在Android6.0及以上的時候。我們該怎么來做,因為我是使用RxJava的。所以這里也推薦一個RxPermissions來進行獲取權限。

RxPermissions項目地址
還有簡書上達達達達sky 寫的基于Rxjava 1.x的基礎上的RxPermissions源碼解析
(其中最新的RxPermissions中,RxPermissions.getInstance(this)方法,改為了new RxPermissions(this))

那我們簡單來看下是怎么使用:

new RxPermissions(UpdateActivity.this)
    .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.INTERNET)
    .subscribe(new Action1<Boolean>() {
         @Override
        public void call(Boolean aBoolean) {
            //當用戶按了確定按鈕,aBoolean為true,否則為false
            if (aBoolean) {
                //成功授予權限,則跳出提示框,是否下載APK
                createAlert();
            } else {
                //用戶拒絕同意授予權限
                Toast.makeText(UpdateActivity.this, "權限不足,無法下載更新。", Toast.LENGTH_LONG).show();
            }
        }
    });

這里就提一點:request方法是當申請多個權限的時候,只要有一個權限用戶不同意授予,aBoolean就會為false,如果想要為每個權限的授予專門做處理,可以把request改為requestEach。更多的使用還是請看上面的相關文章鏈接。

注意:由于在請求權限的過程中app有可能會被重啟,所以權限請求必須放在初始化的階段,比如在Activity.onCreate/onResume, 或者View.onFinishInflate方法中。如果不這樣處理,那么如果app在請求過程中重啟的話,權限請求結果將不會發送給訂閱者即subscriber。

4.好了。現在我們也已經把下載APK的所需的權限也搞定了,當用戶同意授予相應的權限的時候,接下去就是跳出對話框,詢問用戶是否需要更新APK,這里就是單純的創建一個對話框詢問即可,估計大家都會,直接上代碼:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示標題");
builder.setMessage("提示內容");
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialogInterface, int i) {
        dialogInterface.dismiss();
        Toast.makeText(UpdateActivity.this, "取消更新。", Toast.LENGTH_LONG).show();
    }
});

builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialogInterface, int i) {
        //進入下一步,去確定是WiFi還是流量
        confirmWifi();
    }
});

//讓對話框不能通過點擊返回按鈕或者其他區域讓對話框消失
builder.setCancelable(false);

builder.create().show();

5.用戶如果點擊確定按鈕。然后我們這時候就要判斷,是不是WiFi情況下,如果是WiFi情況下就直接進行更新,如果不是,再創建對話框,然后詢問用戶,是否確定需要通過流量來進行下載:

    public void confirmWifi(){
        if(isWiFi(UpdateActivity.this)){
            startService(new Intent(UpdateActivity.this, UpdateService.class));
            Toast.makeText(UpdateActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
        }else{
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("提示");
            builder.setMessage("是否要用流量進行下載更新");
            builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dialogInterface.dismiss();
                    Toast.makeText(UpdateActivity.this, "取消更新。", Toast.LENGTH_LONG).show();
                }
            });

            builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    startService(new Intent(UpdateActivity.this, UpdateService.class));
                    Toast.makeText(UpdateActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
                }
            });
            builder.setCancelable(false);
            builder.create().show();
        }
    }


    //判斷是不是WiFi狀態
    public static boolean isWiFi(Context cxt) {
        ConnectivityManager cm = (ConnectivityManager) cxt
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        // wifi的狀態:ConnectivityManager.TYPE_WIFI
        // 3G的狀態:ConnectivityManager.TYPE_MOBILE
        NetworkInfo.State state = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
                .getState();
        return NetworkInfo.State.CONNECTED == state;
    }

記得查詢當前是不是WiFi狀態也要加權限:

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

然后我們就startService(new Intent(UpdateActivity.this, UpdateService.class));來進行接下去的下載之路,因為一般下載都是在后臺,所以都是放在Service中進行操作的。

這里我順便放篇鏈接,關于Service的,覺得寫得不錯,大家可以看下:
深入理解Android的startservice和bindservice

6.我們前面的條件都ok了。用戶也都按了確定之后,就開始我們正式的下載之路,啟動Service來進行相關的后續操作:
第六個部分我會分幾塊來講解

  • 下載APK --- DownLoadManager

基本的使用及介紹大家看下面文章介紹:
Android系統下載管理DownloadManager

所以我們通過DownLoadManager來進行APK的下載,代碼如下:

public void downApk() {

    //當發現本地以及有該APK的時候先進行刪除再下載,不然下載下來多次之后手機自動會變成Chint-1.apk,Chint-2.apk等
    File apkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()+"/Chint.apk");
    if(apkFile.exists()){
        Toast.makeText(this,"已經有apk存在,將要刪除",Toast.LENGTH_LONG).show();
        apkFile.delete();
    }

    DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
    DownloadManager.Request request = new DownloadManager.Request(Uri.parse(DOWNURL));
    request.setMimeType("application/vnd.android.package-archive");
    //request.setDescription("XXXX");
    //request.setTitle("XXX");
    request.setDestinationInExternalPublicDir(
            Environment.DIRECTORY_DOWNLOADS, "Chint.apk");
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
    manager.enqueue(request);
}

(題外話:還有一種下載是通過瀏覽器去下載:
瀏覽器下載
將下載鏈接使用瀏覽器打開,把下載任務交給瀏覽器,讓瀏覽器調用系統下載器去下載,下載過程在通知欄有下載進度,下載完后文件通常存放在 “外部存儲器” 根目錄下的 download 文件夾, 也就是: /mnt/sdcard/download。
打開下載鏈接的 Intent:

Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setData(Uri.parse("下載鏈接"));
startActivity(intent);

使用這種方法下載完全把工作交給了系統應用,自己的應用中不需要申請任何權限,方便簡單快捷。但如此我們也不能知道下載文件的大小,不能監聽下載進度和下載結果。本Demo中沒使用,當然這個也可以。

  • 如何知道下載完成
    我們已經把APK下載下來了,那我們需要再APK下載完成后進行安裝,那我們什么時候知道APK下載完成呢,讓我們來看下有沒有方法可以用,當然有方法可以知道 (這B裝的我好累,休息一下。),當DownLoadManager下載完成后,會發送一個DownloadManager.ACTION_DOWNLOAD_COMPLETE的廣播,所以我們只要剛開始在啟動Service的時候,注冊一個廣播,監聽
    DownloadManager.ACTION_DOWNLOAD_COMPLETE,然后當下載完成后,在BroadcastReceiver中調用安裝APK的方法即可。是不是很方便。
public void receiverRegist() {
    receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            installApk(context);
            stopSelf();
        }
    };

    IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
    registerReceiver(receiver, filter);

}

然后進行安裝APK,安裝結束后調用stopSelf();來摧毀這個Service當Service被摧毀的時候,要記得注銷這個廣播哦:

@Override
public void onDestroy() {
    unregisterReceiver(receiver);
    super.onDestroy();
}
  • 安裝APK:
public void installApk(Context context) {
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setDataAndType(
            Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(
                Environment.DIRECTORY_DOWNLOADS), "Chint.apk")),
                "application/vnd.android.package-archive");

    context.startActivity(intent);
}

這里額外再傳送個鏈接,如果想卸載軟件咋辦,請看下面文章鏈接:
Android程序中實現APK的安裝與卸載

這里當自動安裝下載下來的APK的時候,因為用的默認 debug 證書簽名的 apk 來測試,下載更新了服務器上的用正式證書簽名的 apk,發現 apk 下載下來了,但是安裝失敗,后來發現是證書原因,我原本一直以為只要包名一樣,就默認為同一個 app,就會安裝自動覆蓋,沒想到錯了。大家可以查看鏈接:
Android簽名與程序覆蓋問題

這里安裝APK的時候要提下Android 7.0的特殊情況:

因為7.0之后權限變得更加嚴格,通過Intent來安裝APK需要添加一個Provider,這里我Demo沒寫,給出下面文章鏈接,大家可以看下(下面第一篇里面也說明了為什么7.0下用普通的Intent安裝會報錯):

Android7.0適配教程,心得

如何在Android7.0系統下通過Intent安裝apk


最后上一下代碼全文
UpdateActivity.java:

package yunyuan.androiddemo.appupdate;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.widget.Toast;

import com.tbruyelle.rxpermissions.RxPermissions;

import rx.functions.Action1;
import yunyuan.androiddemo.R;

/**
 * Created by willy on 17/1/10.
 */

public class UpdateActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_update);

        new RxPermissions(UpdateActivity.this)
                .request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.INTERNET)
                .subscribe(new Action1<Boolean>() {
                    @Override
                    public void call(Boolean aBoolean) {
                        if (aBoolean) {
                            createAlert();
                        } else {
                            Toast.makeText(UpdateActivity.this, "權限不足,無法下載更新。", Toast.LENGTH_LONG).show();
                        }
                    }
                });


    }


    public void createAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示標題");
        builder.setMessage("提示內容");
        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                dialogInterface.dismiss();
                Toast.makeText(UpdateActivity.this, "取消更新。", Toast.LENGTH_LONG).show();
            }
        });

        builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                confirmWifi();
            }
        });
        builder.setCancelable(false);

        builder.create().show();
    }


    public void confirmWifi() {
        if (isWiFi(UpdateActivity.this)) {
            startService(new Intent(UpdateActivity.this, UpdateService.class));
            Toast.makeText(UpdateActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
        } else {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("提示");
            builder.setMessage("是否要用流量進行下載更新");
            builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dialogInterface.dismiss();
                    Toast.makeText(UpdateActivity.this, "取消更新。", Toast.LENGTH_LONG).show();
                }
            });

            builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    startService(new Intent(UpdateActivity.this, UpdateService.class));
                    Toast.makeText(UpdateActivity.this, "開始下載。", Toast.LENGTH_LONG).show();
                }
            });
            builder.setCancelable(false);
            builder.create().show();
        }
    }

    public static boolean isWiFi(Context cxt) {
        ConnectivityManager cm = (ConnectivityManager) cxt
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        // wifi的狀態:ConnectivityManager.TYPE_WIFI
        // 3G的狀態:ConnectivityManager.TYPE_MOBILE
        NetworkInfo.State state = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
                .getState();
        return NetworkInfo.State.CONNECTED == state;
    }

}


UpadateService.java

package yunyuan.androiddemo.appupdate;

import android.app.DownloadManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.widget.Toast;

import java.io.File;

/**
 * Created by willy on 17/1/9.
 */

public class UpdateService extends Service {

    public static final String DOWNURL = "http://dakaapp.troila.com/download/daka.apk?v=3.0";
    BroadcastReceiver receiver;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        receiverRegist();
        downApk();
        return Service.START_STICKY;
    }

    public void receiverRegist() {
        receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                installApk(context);
                stopSelf();
            }
        };

        IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
        registerReceiver(receiver, filter);

    }

    public void installApk(Context context) {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setDataAndType(
                Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(
                        Environment.DIRECTORY_DOWNLOADS), "Chint.apk")),
                "application/vnd.android.package-archive");

        context.startActivity(intent);
    }


    public void downApk() {

        File apkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()+"/Chint.apk");
        if(apkFile.exists()){
            Toast.makeText(this,"已經有apk存在,將要刪除",Toast.LENGTH_LONG).show();
            apkFile.delete();
        }

        DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(DOWNURL));
        request.setMimeType("application/vnd.android.package-archive");
        //request.setDescription("XXXX");
        //request.setTitle("XXX");
        request.setDestinationInExternalPublicDir(
                Environment.DIRECTORY_DOWNLOADS, "Chint.apk");
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
        manager.enqueue(request);
    }

    @Override
    public void onDestroy() {
        unregisterReceiver(receiver);
        super.onDestroy();
    }
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,467評論 25 708
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,547評論 0 17
  • “注意左線!”茨密西突然喊道。但當他們回過神來的時候,戰線已經被一群揮舞著巨劍的步兵騎士所突破……“渣崽!僅憑...
    USSR大本營閱讀 1,804評論 0 3
  • 瀏覽器渲染流程1.瀏覽器解析(1)瀏覽器解析HTML,構建DOM樹(2)瀏覽器解析css,構建CSS規則樹(2)解...
    swhzzz閱讀 249評論 0 0
  • 四月季,一切自然而然,安逸之年,你也平常,我也平常。 對生活,最質樸的感受。 一切,法無定法。 沒有絕對的真理,真...
    安狐狐閱讀 374評論 0 2