公司開發時候,應該最常用的就是APP升級功能,倒不是說的是熱修復等技術,而是普通的檢測到服務器版本比本地手機版本高的時候,手機會詢問用戶是否要下載最新的app,然后下載apk下來,然后進行安裝,我以前用的都是別人封裝好的。也沒仔細看過,這次又正好有這個需求,就老老實實自己寫了一下。
(PS:也可以用第三方公司出的,比如騰訊的Bugly等,也挺方便的,不過apk要上傳到Bugly的平臺上,但是有些公司會要求在自己平臺上,這時候這些第三方就無法使用了。)
-------------------------------------我是分割分割君---------------------------------
大家都知道應用升級,也都體驗過應用升級,而開發步驟也一般分為這么幾步(如果圖片里面缺少啥步驟,歡迎指出。):
我們就按照一步步來分析:
- 從服務器上獲取版本信息,怎么做呢,只要和你們后臺開發人員搞好關系即可。哈哈。一般需要他們提供這幾個字段。
{
"versionCode": "1",
"versionName": "1.0",
"apkUrl": "http://java.linuxlearn.net/shelwee/Finances.apk",
"updateTitle": "更新提示" ,
"changeLog":"1.修復xxx Bug;\n2.更新UI界面."
}
- 獲取本地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安裝會報錯):
最后上一下代碼全文
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();
}
}