項(xiàng)目需求討論-Android App升級

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

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


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

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

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

  1. 從服務(wù)器上獲取版本信息,怎么做呢,只要和你們后臺開發(fā)人員搞好關(guān)系即可。哈哈。一般需要他們提供這幾個(gè)字段。
 {   
        "versionCode": "1", 
        "versionName": "1.0", 
        "apkUrl": "http://java.linuxlearn.net/shelwee/Finances.apk",
        "updateTitle": "更新提示" ,
        "changeLog":"1.修復(fù)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;
        }
    }

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

3.我們也看到了,這里我分成了Android6.0為分割線做區(qū)別。因?yàn)锳ndroid6.0開始后,單純的在AndroidManifest.xml中定義權(quán)限已經(jīng)不夠了。需要再代碼中動(dòng)態(tài)讓用戶來確定才能給APP相應(yīng)的權(quán)限。所以我們APP在AndroidManifest.xml中還是定義

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

因?yàn)樵贏ndroid6.0系統(tǒng)下,就等于獲取到了這二者的權(quán)限。(下載APK當(dāng)然要網(wǎng)絡(luò)權(quán)限和把文件寫入存儲的權(quán)限)

那如果在Android6.0及以上的時(shí)候。我們該怎么來做,因?yàn)槲沂鞘褂肦xJava的。所以這里也推薦一個(gè)RxPermissions來進(jìn)行獲取權(quán)限。

RxPermissions項(xiàng)目地址
還有簡書上達(dá)達(dá)達(dá)達(dá)sky 寫的基于Rxjava 1.x的基礎(chǔ)上的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) {
            //當(dāng)用戶按了確定按鈕,aBoolean為true,否則為false
            if (aBoolean) {
                //成功授予權(quán)限,則跳出提示框,是否下載APK
                createAlert();
            } else {
                //用戶拒絕同意授予權(quán)限
                Toast.makeText(UpdateActivity.this, "權(quán)限不足,無法下載更新。", Toast.LENGTH_LONG).show();
            }
        }
    });

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

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

4.好了?,F(xiàn)在我們也已經(jīng)把下載APK的所需的權(quán)限也搞定了,當(dāng)用戶同意授予相應(yīng)的權(quán)限的時(shí)候,接下去就是跳出對話框,詢問用戶是否需要更新APK,這里就是單純的創(chuàng)建一個(gè)對話框詢問即可,估計(jì)大家都會,直接上代碼:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示標(biāo)題");
builder.setMessage("提示內(nèi)容");
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) {
        //進(jìn)入下一步,去確定是WiFi還是流量
        confirmWifi();
    }
});

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

builder.create().show();

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

    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("是否要用流量進(jìn)行下載更新");
            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狀態(tài)
    public static boolean isWiFi(Context cxt) {
        ConnectivityManager cm = (ConnectivityManager) cxt
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        // wifi的狀態(tài):ConnectivityManager.TYPE_WIFI
        // 3G的狀態(tài):ConnectivityManager.TYPE_MOBILE
        NetworkInfo.State state = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
                .getState();
        return NetworkInfo.State.CONNECTED == state;
    }

記得查詢當(dāng)前是不是WiFi狀態(tài)也要加權(quán)限:

<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));來進(jìn)行接下去的下載之路,因?yàn)橐话阆螺d都是在后臺,所以都是放在Service中進(jìn)行操作的。

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

6.我們前面的條件都o(jì)k了。用戶也都按了確定之后,就開始我們正式的下載之路,啟動(dòng)Service來進(jìn)行相關(guān)的后續(xù)操作:
第六個(gè)部分我會分幾塊來講解

  • 下載APK --- DownLoadManager

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

所以我們通過DownLoadManager來進(jìn)行APK的下載,代碼如下:

public void downApk() {

    //當(dāng)發(fā)現(xiàn)本地以及有該APK的時(shí)候先進(jìn)行刪除再下載,不然下載下來多次之后手機(jī)自動(dòng)會變成Chint-1.apk,Chint-2.apk等
    File apkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath()+"/Chint.apk");
    if(apkFile.exists()){
        Toast.makeText(this,"已經(jīng)有apk存在,將要?jiǎng)h除",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);
}

(題外話:還有一種下載是通過瀏覽器去下載:
瀏覽器下載
將下載鏈接使用瀏覽器打開,把下載任務(wù)交給瀏覽器,讓瀏覽器調(diào)用系統(tǒng)下載器去下載,下載過程在通知欄有下載進(jìn)度,下載完后文件通常存放在 “外部存儲器” 根目錄下的 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);

使用這種方法下載完全把工作交給了系統(tǒng)應(yīng)用,自己的應(yīng)用中不需要申請任何權(quán)限,方便簡單快捷。但如此我們也不能知道下載文件的大小,不能監(jiān)聽下載進(jìn)度和下載結(jié)果。本Demo中沒使用,當(dāng)然這個(gè)也可以。

  • 如何知道下載完成
    我們已經(jīng)把APK下載下來了,那我們需要再APK下載完成后進(jìn)行安裝,那我們什么時(shí)候知道APK下載完成呢,讓我們來看下有沒有方法可以用,當(dāng)然有方法可以知道 (這B裝的我好累,休息一下。),當(dāng)DownLoadManager下載完成后,會發(fā)送一個(gè)DownloadManager.ACTION_DOWNLOAD_COMPLETE的廣播,所以我們只要?jiǎng)傞_始在啟動(dòng)Service的時(shí)候,注冊一個(gè)廣播,監(jiān)聽
    DownloadManager.ACTION_DOWNLOAD_COMPLETE,然后當(dāng)下載完成后,在BroadcastReceiver中調(diào)用安裝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);

}

然后進(jìn)行安裝APK,安裝結(jié)束后調(diào)用stopSelf();來摧毀這個(gè)Service當(dāng)Service被摧毀的時(shí)候,要記得注銷這個(gè)廣播哦:

@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);
}

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

這里當(dāng)自動(dòng)安裝下載下來的APK的時(shí)候,因?yàn)橛玫哪J(rèn) debug 證書簽名的 apk 來測試,下載更新了服務(wù)器上的用正式證書簽名的 apk,發(fā)現(xiàn) apk 下載下來了,但是安裝失敗,后來發(fā)現(xiàn)是證書原因,我原本一直以為只要包名一樣,就默認(rèn)為同一個(gè) app,就會安裝自動(dòng)覆蓋,沒想到錯(cuò)了。大家可以查看鏈接:
Android簽名與程序覆蓋問題

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

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

Android7.0適配教程,心得

如何在Android7.0系統(tǒng)下通過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, "權(quán)限不足,無法下載更新。", Toast.LENGTH_LONG).show();
                        }
                    }
                });


    }


    public void createAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示標(biāo)題");
        builder.setMessage("提示內(nèi)容");
        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("是否要用流量進(jìn)行下載更新");
            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的狀態(tài):ConnectivityManager.TYPE_WIFI
        // 3G的狀態(tài):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,"已經(jīng)有apk存在,將要?jiǎng)h除",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();
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容