前言
自己寫(xiě)簡(jiǎn)書(shū)記錄知識(shí)與看別人的簡(jiǎn)書(shū)饲握、博客去學(xué)習(xí)有很大的區(qū)別,需要斟酌每一行代碼的編寫(xiě)蚕键,認(rèn)真的總結(jié)在編寫(xiě)代碼過(guò)程中遇到的問(wèn)題救欧,避免以及提醒其他android開(kāi)發(fā)者同樣的問(wèn)題。
其中的艱辛只有自己體會(huì)锣光,但同時(shí)也受益匪淺笆怠。Android的開(kāi)發(fā)學(xué)習(xí)就是在不斷的總結(jié)、思考中誊爹,在不斷的分享中才能得到進(jìn)步蹬刷。致那些無(wú)私奉獻(xiàn)瓢捉、不辭辛苦的開(kāi)源開(kāi)發(fā)者,博主办成,答主泡态,簡(jiǎn)主。予人樂(lè)迂卢,其樂(lè)無(wú)窮某弦。
代碼無(wú)可期,夢(mèng)想尤可違而克。但使志常存靶壮,亦復(fù)為君啟。
PS:自己總結(jié)的一句話
本篇內(nèi)容較多员萍,涵蓋的知識(shí)點(diǎn)較廣腾降,建議收藏學(xué)習(xí)。
Android6.0/7.0系統(tǒng)軟件版本使用Tomcat服務(wù)器進(jìn)行版本檢測(cè)更新
版本的升級(jí)檢測(cè)作為每個(gè)上線APP必備的功能碎绎,其重要性不言而喻部宿。很多的開(kāi)發(fā)者苦于不懂后端的開(kāi)發(fā)们何,無(wú)法進(jìn)行本地的版本測(cè)試借浊、升級(jí)的操作专肪。本篇將重點(diǎn)講述在Android6.0/7.0及以上系統(tǒng)版本中拇厢,如何利用Tomcat服務(wù)器進(jìn)行本地的軟件版本的檢測(cè)椭岩、更新竟趾。在上一篇中已經(jīng)講述如何利用Tomcat搭建本地服務(wù)器冯事,還沒(méi)有瀏覽這一篇的請(qǐng)自行前往: Android版本更新(一)---Tomcat服務(wù)器安裝配置及問(wèn)題解決宿接。鑒于Android系統(tǒng)的開(kāi)源性赘淮、資源的廣泛性,以下內(nèi)容中也會(huì)引用到其他開(kāi)發(fā)者的相關(guān)博客睦霎、簡(jiǎn)書(shū)梢卸,如有侵權(quán)還請(qǐng)告知,并及時(shí)修改副女。
本章節(jié)將通過(guò)以下幾個(gè)模塊來(lái)介紹如何進(jìn)行進(jìn)行版本的檢測(cè)蛤高、更新:
- 服務(wù)器端的代碼構(gòu)建
- 本地軟件版本的獲取
- 對(duì)比服務(wù)器端版本彈出升級(jí)對(duì)話框
- 下載并安裝最新版本的軟件
軟件版本檢測(cè)、更新的程序流程框圖:
最終的軟件版本升級(jí)演示如下:
一碑幅、服務(wù)器端代碼構(gòu)建
服務(wù)器端的代碼的構(gòu)建比較簡(jiǎn)單戴陡,只需要?jiǎng)?chuàng)建一個(gè)json格式的文件(保存的字符標(biāo)準(zhǔn)選擇為UTF-8,否則在后續(xù)的Gson解析的時(shí)候中文為亂碼)沟涨,如updateinfo.json恤批,具體代碼如下:
{
"versionName":"2.0",
"versionCode":2,
"des":"這是升級(jí)后的版本",
"apkUrl":"http://172.26.0.1:8181/app-release.apk"
}
其文件放置的位置為,tomcat根目錄下裹赴,主要包含兩個(gè)文件updateinfo.json和app-release.apk喜庞;
updateinfo.json---包含升級(jí)的相關(guān)信息
versionName:版本名诀浪,可用于在軟件中顯示的版本名稱;
versionCode:版本號(hào)延都,用于本地versionCode與服務(wù)器端的對(duì)比雷猪;
des:版本升級(jí)的一些信息;
apkUrl:即升級(jí)的軟件安裝包app-release.apk所在的服務(wù)器端的位置連接晰房;
app-release.apk---待升級(jí)的版本
以上兩個(gè)文件可以僅作為測(cè)試使用求摇,可以根據(jù)需求自行設(shè)置內(nèi)容及文件名。
打開(kāi)Tomcat嫉你,并通過(guò)cmd的ipconfig獲取本地的ip地址
然后在瀏覽器輸入: http://172.26.0.1:8181/updateinfo.json月帝,即可顯示我們的服務(wù)器的升級(jí)信息。
注意:這個(gè)是我本地的IP地址幽污,請(qǐng)注意修改為你本地的IP地址嚷辅,至于端口號(hào)為什么是8181,在上一篇中有說(shuō)到距误,這個(gè)是由于本地的8080端口被占用簸搞,瀏覽器中的中文出現(xiàn)亂碼,這個(gè)是瀏覽器的問(wèn)題准潭,不用理會(huì)趁俊。
二、本地軟件版本的獲取
在項(xiàng)目project的AndroidManifest.xml中設(shè)置當(dāng)前軟件的版本信息versionCode和versionName
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test"
android:versionName="1.0"
android:versionCode="1">
同時(shí)需要module的defaultConfig配置versionCode和versionName
構(gòu)建工具類刑然,來(lái)獲取本地的versionCode和versionName
/**
* 獲取本地的versionCode和versionName
*/
public class VersionGetUtil {
private static final String TAG = "VersionGetUtil";
/**
* 獲取版本名
*/
public static String getVersionName(Context context){
//PackageManager,可以獲取清單中的所有信息
PackageManager manager = context.getPackageManager();
//getPackageName(),獲取當(dāng)前程序的包名
try {
//獲取包中的信息
PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
String versionName = info.versionName;//版本名寺擂,是需要在APP中顯示的
Log.i(TAG, "getVersion: name" + versionName);
return versionName;
}catch (PackageManager.NameNotFoundException e){
e.printStackTrace();
Log.e("VersionGetUtil","can not get current Version Name");
}
//如果出現(xiàn)異常拋出null
return null;
}
/**
* 獲取版本號(hào)
*/
public static int getVersionCode(Context context){
//PackageManager,可以獲取清單中的所有信息
PackageManager manager = context.getPackageManager();
//getPackageName(),獲取當(dāng)前程序的包名
try {
//獲取包中的信息
PackageInfo info = manager.getPackageInfo(context.getPackageName(),0);
int versionCode = info.versionCode;//版本號(hào),用于判斷是否為最新版本
Log.i(TAG, "getVersion: code" + versionCode);
return versionCode;
}catch (PackageManager.NameNotFoundException e){
e.printStackTrace();
Log.e("VersionGetUtil","can not get current Version Code");
}
//如果出現(xiàn)異常拋出0
return 0;
}
}
三泼掠、對(duì)比服務(wù)器端版本彈出升級(jí)對(duì)話框等操作
首先根據(jù)服務(wù)器端的json格式的版本升級(jí)數(shù)據(jù)怔软,封裝成一個(gè)實(shí)體類VersionInfoEntity,便于后續(xù)使用OkHttp請(qǐng)求的時(shí)候解析择镇,可以使用GsonFormat快速的實(shí)現(xiàn)實(shí)體類的生成挡逼,可以參考: 插件GsonFormat快速生成JSon實(shí)體類,
public class VersionInfoEntity {
/**
* versionName : 2.0
* versionCode : 2
* des : 這是升級(jí)后的版本
* apkUrl : http://172.26.0.1:8181/app-release.apk
*/
private String versionName;
private int versionCode;
private String des;
private String apkUrl;
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public int getVersionCode() {
return versionCode;
}
public void setVersionCode(int versionCode) {
this.versionCode = versionCode;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public String getApkUrl() {
return apkUrl;
}
public void setApkUrl(String apkUrl) {
this.apkUrl = apkUrl;
}
}
軟件版本的升級(jí)腻豌,做法很簡(jiǎn)單:通過(guò)獲取本地的版本號(hào)或者版本名與服務(wù)器端的版本號(hào)或者版本名進(jìn)行對(duì)比家坎,不一致則提示升級(jí)等操作,這里就只講述版本號(hào)對(duì)比吝梅∈瑁考慮到其內(nèi)部的流程比較多,且為了后續(xù)的維護(hù)憔涉,我的做法是封裝成一個(gè)工具類订框,具體代碼如下:
封裝的工具類使用到了OkHttp,handler消息處理兜叨,對(duì)于各個(gè)函數(shù)的使用和關(guān)鍵代碼都有注釋穿扳,很容易理解衩侥,就不作一一說(shuō)明,可以參考流程圖對(duì)比代碼矛物,這樣看起來(lái)就會(huì)一目了然茫死。
public class VersionUpdateUtil extends Activity{
private static final String TAG = VersionUpdateUtil.class.getSimpleName();
/**
* 服務(wù)器端保存版本更新信息的地址
*/
private static final String server_url = "http://172.26.0.1:8181/updateinfo.json";
/**
* 用于構(gòu)造函數(shù)
* versionCode: 本地版本號(hào)
*/
private Activity activity;
private int currentVersionCode;
/**
* 版本更新實(shí)體類,包括版本號(hào)履羞,版本名稱峦萎,描述,下載地址
*/
private VersionInfoEntity versionInfoEntity;
/**
* 服務(wù)器版本號(hào),并初始化
*/
private static int serverVersionCode = 1;
/**
* 聲明okhttp客戶端忆首,并設(shè)置讀/寫(xiě)/連接超時(shí)
*/
private OkHttpClient client = new OkHttpClient.Builder()
.readTimeout(10, TimeUnit.SECONDS) //設(shè)置讀超時(shí)
.writeTimeout(10, TimeUnit.SECONDS) //設(shè)置寫(xiě)超時(shí)
.connectTimeout(10, TimeUnit.SECONDS) //設(shè)置連接超時(shí)范圍
.build();
/**
* 忽略版本爱榔,保存服務(wù)器端版本號(hào)到sp中
*/
private SharedPreferences sharedPreferences = null;
private SharedPreferences.Editor editor = null;
private static int savedVersion = 1;
/**
* 進(jìn)度更新dialog
*/
private ProgressDialog progressDialog;
/**
* 請(qǐng)求碼,用于動(dòng)態(tài)權(quán)限設(shè)置的回調(diào)
*/
private static final int REQUEST_CODE = 1;
private File file = null;
/**
* handler消息的處理
*/
private static final int UPDATE_YES = 1;
private static final int UPDATE_NO = 2;
private static final int IO_ERROR = 3;
private static final int SHOW_DIALOG = 4;
private static final int UPDATE_IGNORE = 5;
private static final int UPDATE_PROGRESS = 6;
private static final int UPDATE_INSTALL = 7;
private static final int UPDATE_ADD_PERMISSION = 8;
private static final int NEWEST_VERSION = 9;
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case UPDATE_YES:
Log.i(TAG, "handleMessage: 需要更新");
//從服務(wù)器端獲取apk的下載網(wǎng)址
downLoadApk(versionInfoEntity.getApkUrl());
break;
case UPDATE_NO:
Log.w(TAG, "handleMessage: 不需要更新");
break;
case UPDATE_IGNORE:
Toast.makeText(activity,"用戶忽略了該版本", Toast.LENGTH_SHORT).show();
saveNewestVersion(serverVersionCode);
break;
case IO_ERROR:
Log.e(TAG, "handleMessage: IO異常");
break;
case SHOW_DIALOG:
Log.i(TAG, "handleMessage: 彈出更新對(duì)話框");
showUpdateDialog();
break;
case UPDATE_PROGRESS:
int progress = msg.arg1;
progressDialog.setProgress(progress);
break;
case UPDATE_INSTALL:
Log.i(TAG, "handleMessage: file: " + file);
installApk(activity,file);
break;
case UPDATE_ADD_PERMISSION:
Toast.makeText(activity,"需要添加權(quán)限,請(qǐng)點(diǎn)擊允許", Toast.LENGTH_SHORT).show();
break;
case NEWEST_VERSION:
Toast.makeText(activity,"已經(jīng)是最新版本糙及!", Toast.LENGTH_SHORT).show();
break;
}
}
};
/**
* 構(gòu)造函數(shù)
*
* @param currentVersionCode 本地版本號(hào)
* @param activity
*/
public VersionUpdateUtil(int currentVersionCode, Activity activity) {
this.activity = activity;
this.currentVersionCode = currentVersionCode;
}
/**
* 獲取服務(wù)器版本號(hào)
*/
public void getServerVersionCode() {
//構(gòu)造request详幽,并設(shè)置request參數(shù)
final Request request = new Request.Builder()
.url(server_url)
.build();
//請(qǐng)求調(diào)度,異步get請(qǐng)求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
handler.sendEmptyMessage(IO_ERROR);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
String result = response.body().string();
Log.i(TAG, "當(dāng)前響應(yīng)的結(jié)果:" + result);
//利用gson解析服務(wù)器端的數(shù)據(jù),并將數(shù)據(jù)保存到VersionInfoEntity實(shí)體類中
Gson gson = new Gson();
versionInfoEntity = gson.fromJson(result, VersionInfoEntity.class);
//獲取服務(wù)器端的版本號(hào)與本地的服務(wù)端版本號(hào)作對(duì)比
serverVersionCode = versionInfoEntity.getVersionCode();
Log.i(TAG, "服務(wù)器端版本: " + serverVersionCode);
Log.i(TAG, "本地版本: " + currentVersionCode);
Log.i(TAG, "sp保存的版本:" + savedVersion);
/**
* 版本更新的判斷與是否執(zhí)行了忽略版本的操作
*/
if (serverVersionCode > currentVersionCode) {
if (serverVersionCode == savedVersion){
Log.i(TAG, "onResponse: 用戶選擇了忽略該版本");
handler.sendEmptyMessage(UPDATE_IGNORE);
}else {
handler.sendEmptyMessage(SHOW_DIALOG);
}
} else {
System.out.println("無(wú)最新版本");
handler.sendEmptyMessage(NEWEST_VERSION);
}
}
});
}
/**
* 彈出對(duì)話框浸锨,讓用戶判斷是否需要更新版本
*/
private void showUpdateDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle("監(jiān)測(cè)到新版本");
builder.setMessage(versionInfoEntity.getDes());
builder.setPositiveButton("確定更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
handler.sendEmptyMessage(UPDATE_YES);
}
});
builder.setNeutralButton("忽略版本", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
handler.sendEmptyMessage(UPDATE_IGNORE);
}
});
builder.setNegativeButton("暫不更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int i) {
handler.sendEmptyMessage(UPDATE_NO);
dialog.dismiss();
}
});
builder.create().show();
}
/**
* 從服務(wù)器下載新版本的APK
*/
private void downLoadApk(final String downLoadApkUrl) {
//創(chuàng)建進(jìn)度對(duì)話框
createProgressDialog();
//請(qǐng)求服務(wù)器端的apk
final Request request = new Request.Builder()
.url(downLoadApkUrl)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
handler.sendEmptyMessage(IO_ERROR);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
/**
* android6.0系統(tǒng)后增加運(yùn)行時(shí)權(quán)限唇聘,需要?jiǎng)討B(tài)添加內(nèi)存卡讀取權(quán)限
*/
if (Build.VERSION.SDK_INT >= 23) {
int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
progressDialog.dismiss();
ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
Log.w(TAG, "checkWriteStoragePermission: 無(wú)此權(quán)限,需要添加");
handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
return;
} else {
downApkFlie(response);
if (progressDialog != null && progressDialog.isShowing()){
progressDialog.dismiss();
}
handler.sendEmptyMessage(UPDATE_INSTALL);
}
} else {
downApkFlie(response);
if (progressDialog != null && progressDialog.isShowing()){
progressDialog.dismiss();
}
handler.sendEmptyMessage(UPDATE_INSTALL);
}
}
});
}
/**
* 忽略當(dāng)前服務(wù)器端的版本
* @param versionCode
*/
private void saveNewestVersion(int versionCode) {
sharedPreferences = activity.getSharedPreferences("ignore_ServerVersionCode", Activity.MODE_PRIVATE);
editor = sharedPreferences.edit();
editor.putInt("ignore_ServerVersionCode", versionCode);
editor.commit();
savedVersion = sharedPreferences.getInt("ignore_ServerVersionCode",versionCode);
}
private void createProgressDialog() {
progressDialog = new ProgressDialog(activity);
progressDialog.setMax(100);
progressDialog.setCancelable(false);
progressDialog.setMessage("正在下載");
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.show();
}
/**
* OkHttp請(qǐng)求的結(jié)果
*
* @param response
*/
private void downApkFlie(Response response) {
InputStream is = null;
FileOutputStream fos = null;
byte[] buf = new byte[1024];//每次讀取1K的數(shù)據(jù)
int len = 0;
long sum = 0;
int progress = 0;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
file = new File(Environment.getExternalStorageDirectory(), "test.apk");
try {
if (file.exists()) {
file.delete();
} else {
file.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
long total = response.body().contentLength();
Log.i(TAG, "downApkFlie: total---" + total);
is = response.body().byteStream();
//捕捉是否動(dòng)態(tài)分配讀寫(xiě)內(nèi)存權(quán)限異常
try {
fos = new FileOutputStream(file);
//捕捉輸入流讀取異常
try {
/**
* read(),從輸入流中讀取數(shù)據(jù)的下一個(gè)字節(jié)柱搜,返回0~255范圍內(nèi)的字節(jié)值迟郎,如果已經(jīng)到達(dá)
* 流末尾而沒(méi)有可用的字節(jié),則返回-1
*/
while ((len = is.read(buf)) != -1) {
fos.write(buf, 0, len);//write(byte[]b, off, int len), 將指定的byte數(shù)組中從偏移量off開(kāi)始的len個(gè)字節(jié)寫(xiě)入此輸出流
sum += len;
progress = (int) (sum * 1.0f / total * 100);
Log.d("h_bl", "progress=" + progress);
//更新進(jìn)度
Message msg = handler.obtainMessage();
msg.what = UPDATE_PROGRESS;
msg.arg1 = progress;
handler.sendMessage(msg);
}
fos.flush();//徹底完成輸出并清空緩存區(qū)
Log.i(TAG, "downApkFlie: 下載完畢");
} catch (IOException e) {
handler.sendEmptyMessage(IO_ERROR);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "downApkFlie: 下載失敗");
} finally {
//清空f(shuō)ile輸入輸出流
try {
if (is != null) {
is.close();//關(guān)閉輸入流
}
if (fos != null) {
fos.close();//關(guān)閉輸出流
}
} catch (IOException e) {
handler.sendEmptyMessage(IO_ERROR);
}
}
}
}
/**
* 安裝新版本APK
*/
protected void installApk(Activity activity, File file) {
if (activity == null || !file.exists()){
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
// 由于沒(méi)有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的權(quán)限問(wèn)題
//賦予臨時(shí)權(quán)限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//通過(guò)provider生成uri
Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test為自己的包名
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
activity.startActivity(intent);
}
}
在以上封裝的工具類中聪蘸,遇到了很多問(wèn)題宪肖,重點(diǎn)講述兩個(gè)問(wèn)題:
- 動(dòng)態(tài)權(quán)限;
- Android7.0安裝apk健爬;
3.1 Android6.0動(dòng)態(tài)權(quán)限
在上述工具類VersionInfoEntity.class中匈庭,在確定更新后,進(jìn)入到downLoadApk()浑劳,從服務(wù)器端下載最新的apk。起初夭拌,我在Androidmanifest.xml中定義了以下權(quán)限
<!--相關(guān)權(quán)限-->
<!-- 權(quán)限設(shè)置 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<!-- 在SD卡中創(chuàng)建和刪除文件權(quán)限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/>
<!-- 向SD卡中寫(xiě)入東西權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>-->
<!-- 藍(lán)牙 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<!-- 網(wǎng)絡(luò)權(quán)限 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
這個(gè)習(xí)慣很不好魔熏,其實(shí)很多的權(quán)限,可能就用不到鸽扁,但是經(jīng)常怕遺忘蒜绽,就全部拷貝過(guò)來(lái)了,那么問(wèn)題就來(lái)了桶现,在編譯運(yùn)行的時(shí)候躲雅,打印以下log:
FileNotFoundException:/storage/emulated/0/test.apk,(Permission denied)
沒(méi)有權(quán)限?可是我明明已經(jīng)給與寫(xiě)內(nèi)存卡的權(quán)限了啊骡和,仔細(xì)地查詢了一些資料相赁,以及在android開(kāi)發(fā)者官方: developer
Android6.0為了保護(hù)用戶的隱私相寇,將一些權(quán)限的申請(qǐng)放在了應(yīng)用運(yùn)行的時(shí)候去申請(qǐng),如內(nèi)存卡的讀寫(xiě)權(quán)限钮科。在以前的版本中唤衫,開(kāi)發(fā)人員只需要在AndroidManifest.xml中設(shè)置即可,如
<!-- 向SD卡中寫(xiě)入東西權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
安裝應(yīng)用的時(shí)候可以在設(shè)置的應(yīng)用信息中看到绵脯,如應(yīng)用需要獲得***權(quán)限佳励,用戶點(diǎn)擊后就可以設(shè)置相應(yīng)的權(quán)限,如允許蛆挫、拒絕等赃承。但是存在這一的一個(gè)問(wèn)題,如一款應(yīng)用APP只需要電話悴侵、短信的權(quán)限瞧剖,但是在開(kāi)發(fā)的過(guò)程中,開(kāi)發(fā)者為了省事畜挨,請(qǐng)求了全部的權(quán)限筒繁,這就可能導(dǎo)致侵犯了用戶隱私的權(quán)限請(qǐng)求,在用戶安裝了這款A(yù)PP后巴元,才發(fā)現(xiàn)拍照毡咏、讀取內(nèi)存、網(wǎng)絡(luò)等權(quán)限被打開(kāi)了逮刨,有可能導(dǎo)致一些隱私數(shù)據(jù)被剽竊呕缭。而Google官網(wǎng)為了避免用戶的數(shù)據(jù)被剽竊,在Android6.0版本后加入了動(dòng)態(tài)權(quán)限的的申請(qǐng)修己。 對(duì)于我這種喜歡全部權(quán)限都給予的人也是一種約束恢总。
這些動(dòng)態(tài)的權(quán)限在需要的時(shí)候才需要用戶動(dòng)態(tài)申請(qǐng),比如在上面所說(shuō)的APP中睬愤,如果需要用到拍照的功能片仿,需要在使用的地方通過(guò)代碼請(qǐng)求打開(kāi)拍照權(quán)限的方式動(dòng)態(tài)的去請(qǐng)求這個(gè)拍照權(quán)限。
以下是危險(xiǎn)權(quán)限尤辱,有組的概念砂豌,如果一個(gè)權(quán)限組內(nèi)的某個(gè)權(quán)限被獲取了,那么這個(gè)組中剩余的權(quán)限也會(huì)被自動(dòng)獲取光督。而這些權(quán)限需要?jiǎng)討B(tài)的去申請(qǐng)阳距,可以理解為,動(dòng)態(tài)的申請(qǐng)了
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.STORAGE结借,組中的其他權(quán)限也將自動(dòng)獲得筐摘。
以下是一些比較危險(xiǎn)的權(quán)限,需要去動(dòng)態(tài)的申請(qǐng):
//聯(lián)系人
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
//電話
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
//日歷
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
//相機(jī)
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
//傳感器
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
//定位
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
//內(nèi)存卡
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
//耳機(jī)
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
//SMS
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
因此將從服務(wù)器端下載apk這一塊需要用到動(dòng)態(tài)權(quán)限的地方特殊的解釋一下:代碼如下:
/**
* android6.0系統(tǒng)后增加運(yùn)行時(shí)權(quán)限,需要?jiǎng)討B(tài)添加內(nèi)存卡讀取權(quán)限
*/
if (Build.VERSION.SDK_INT >= 23) {
int permission = ContextCompat.checkSelfPermission(activity, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permission != PackageManager.PERMISSION_GRANTED) {
progressDialog.dismiss();
ActivityCompat.requestPermissions(activity, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE);
Log.w(TAG, "checkWriteStoragePermission: 無(wú)此權(quán)限咖熟,需要添加");
handler.sendEmptyMessage(UPDATE_ADD_PERMISSION);
return;
} else {
downApkFlie(response);
if (progressDialog != null && progressDialog.isShowing()){
progressDialog.dismiss();
}
handler.sendEmptyMessage(UPDATE_INSTALL);
}
} else {
downApkFlie(response);
if (progressDialog != null && progressDialog.isShowing()){
progressDialog.dismiss();
}
handler.sendEmptyMessage(UPDATE_INSTALL);
}
- 檢查權(quán)限
ContextCompat.checkSelfPermission(Context context, String permission)圃酵;
- 有權(quán)限: PackageManager.PERMISSION_GRANTED
- 無(wú)權(quán)限: PackageManager.PERMISSION_DENIED
當(dāng)應(yīng)用需要用到危險(xiǎn)權(quán)限時(shí),在執(zhí)行權(quán)限相關(guān)代碼前球恤,使用該方法判斷是否擁有指定的權(quán)限辜昵。有權(quán)限,則繼續(xù)執(zhí)行設(shè)計(jì)需要權(quán)限的代碼咽斧;無(wú)權(quán)限堪置,則向用戶請(qǐng)求授予權(quán)限。
- 解釋權(quán)限
ActivityCompat.shouldShowRequestPermissionRationale(Activity activity, String permission)
判斷是否有必要向用戶解釋為什么要這項(xiàng)權(quán)限张惹。如果應(yīng)用第一次請(qǐng)求過(guò)此權(quán)限舀锨,但是被用戶拒絕了,則之后調(diào)用該方法將返回 true宛逗,此時(shí)就有必要向用戶詳細(xì)說(shuō)明需要此權(quán)限的原因
備注:如果應(yīng)用第一次請(qǐng)求此權(quán)限時(shí)被用戶拒絕坎匿,第二次再請(qǐng)求此權(quán)限時(shí),用戶勾選了權(quán)限請(qǐng)求對(duì)話框的“不再詢問(wèn)”雷激,則此方法返回 false替蔬。如果設(shè)備規(guī)范禁止應(yīng)用擁有該權(quán)限,此方法也返回 false屎暇。
- 請(qǐng)求權(quán)限
ActivityCompat.requestPermissions(Activity activity, String[] permissions, int requestCode)
當(dāng)檢測(cè)到應(yīng)用沒(méi)有指定的權(quán)限時(shí)承桥,調(diào)用此方法向用戶請(qǐng)求權(quán)限。調(diào)用此方法將彈出權(quán)限請(qǐng)求對(duì)話框詢問(wèn)用戶 “允許” 或 “拒絕” 指定的權(quán)限根悼。
- 權(quán)限參數(shù)傳入的是數(shù)組凶异,可以調(diào)用該方法一次請(qǐng)求多個(gè)權(quán)限;傳入的權(quán)限數(shù)組參數(shù)以單個(gè)具體權(quán)限為單位挤巡,但彈框詢問(wèn)用戶授權(quán)時(shí)矿卑,屬于同一權(quán)限組的權(quán)限將自動(dòng)合并詢問(wèn)授權(quán)一次;
- 請(qǐng)求的權(quán)限必須事先在 AndroidManifest.xml 中有聲明母廷,否則調(diào)用此方法請(qǐng)求時(shí)瀑晒,將不彈框徘意,而是直接返回“拒絕”的結(jié)果轩褐;
- 第一次請(qǐng)求權(quán)限時(shí)椎咧,用戶點(diǎn)擊了“拒絕”,第二次再請(qǐng)求該權(quán)限時(shí)蟋座,對(duì)話框?qū)⒊霈F(xiàn)“不再詢問(wèn)”復(fù)選框脚牍,如果用戶勾選了“不再詢問(wèn)”并點(diǎn)擊了“拒絕”诸狭,則之后再請(qǐng)求此權(quán)限組時(shí)將不彈框,而是直接返回“拒絕”的結(jié)果芹彬。
-
處理結(jié)果
請(qǐng)求權(quán)限的結(jié)果返回和接收一個(gè)Activity的返回類似舒帮,重寫(xiě) FragmentActivity 或 (v4) Fragment 中的 onRequestPermissionsResult(...) 方法陡叠。
/**
* 處理權(quán)限請(qǐng)求結(jié)果
*
* @param requestCode
* 請(qǐng)求權(quán)限時(shí)傳入的請(qǐng)求碼枉阵,用于區(qū)別是哪一次請(qǐng)求的
*
* @param permissions
* 所請(qǐng)求的所有權(quán)限的數(shù)組岭妖,
* 例如String permissions = new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
* android.Manifest.permission.CALL_PHONE};
*
* @param grantResults
* 權(quán)限授予結(jié)果昵慌,和 permissions 數(shù)組參數(shù)中的權(quán)限一一對(duì)應(yīng),
* 例已卷,WRITE_EXTERNAL_STORAGE和CALL_PHONE兩個(gè)元素值為兩種情況侧蘸,如下:
* 授予: PackageManager.PERMISSION_GRANTED
* 拒絕: PackageManager.PERMISSION_DENIED
* 可能的結(jié)果有幾種鹉梨,{true,true},{true,false},{false,true},{false,false}
* 如果針對(duì)某一個(gè)直接采用數(shù)組下標(biāo)來(lái)判斷存皂,例如WRITE_EXTERNAL_STORAGE,則為grantResults[0],代表其權(quán)限值
*
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE && grantResults[0] == PackageManager.PERMISSION_GRANTED){
Log.i(TAG, "onRequestPermissionsResult: ++++");
}
}
3.2 android7.0安裝APK
在apk下載后它改,進(jìn)入安裝階段出現(xiàn)以下error
FATAL EXCEPTION: main
Process: com.example.test.release, PID: 19544
android.os.FileUriExposedException: file:///storage/emulated/0/test.apk exposed beyond app through Intent.getData()
搜尋了一些資料:Android7.0行為變更央拖,這是由于Android7.0執(zhí)行了“StrictMode API 政策禁”以及“私有目錄被限制訪問(wèn)”的原因鲜戒,隨著Android版本越來(lái)越高袍啡,Google對(duì)于用戶隱私的保護(hù)力度也越來(lái)越大境输∮毕担可以用FileProvider來(lái)解決這一問(wèn)題嘁扼。
在應(yīng)用間共享文件
對(duì)于面向 Android 7.0 的應(yīng)用趁啸,Android 框架執(zhí)行的 StrictMode API 政策禁止在您的應(yīng)用外部公開(kāi) file:// URI不傅。如果一項(xiàng)包含文件 URI 的 intent 離開(kāi)您的應(yīng)用访娶,則應(yīng)用出現(xiàn)故障崖疤,并出現(xiàn) FileUriExposedException 異常。
要在應(yīng)用間共享文件叮趴,您應(yīng)發(fā)送一項(xiàng) content:// URI眯亦,并授予 URI 臨時(shí)訪問(wèn)權(quán)限搔驼。進(jìn)行此授權(quán)的最簡(jiǎn)單方式是使用 FileProvider 類舌涨。如需了解有關(guān)權(quán)限和共享文件的詳細(xì)信息囊嘉,請(qǐng)參閱共享文件革为。
” StrictMode API 政策” 是指禁止向你的應(yīng)用外公開(kāi) file:// URI震檩。 如果一項(xiàng)包含文件 file:// URI類型 的 Intent 離開(kāi)你的應(yīng)用抛虏,應(yīng)用失敗迂猴,并出現(xiàn) FileUriExposedException 異常沸毁。
現(xiàn)在我們就來(lái)一步一步的解決這個(gè)問(wèn)題息尺。
3.2.1 AndroidManifest.xml清單文件中注冊(cè)provider
provider也是Android四大組件之一掷倔,可以簡(jiǎn)單把它理解為向外提供數(shù)據(jù)的組件,參考: FileProvider API:在項(xiàng)目的AndroidManifest.xml中注冊(cè)勒葱。具體配置代碼如下:
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.test.fileprovider"
android:grantUriPermissions="true"
android:exported="false"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
其中
android:authorities:組件標(biāo)識(shí)凛虽,這個(gè)屬性的com.example.test為你本項(xiàng)目的包名凯旋,可以在mainfest中找到,用于避免和其它應(yīng)用發(fā)生沖突
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test"
android:versionName="1.0"
android:versionCode="1">
android:resource糠聪,指的是當(dāng)前組件引用 res/xml/file_paths.xml 這個(gè)文件舰蟆,通過(guò)<meta-data>標(biāo)簽將上面的filepath添加到provider當(dāng)中
通過(guò)閱讀API身害,放置<paths>元素和子元素到項(xiàng)目中的xml文件塌鸯,需自行創(chuàng)建
新建的file_paths.xml的代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="test" path="."/>
</paths>
通過(guò)閱讀API:
<files-path/>代表的根目錄:Context.getFilesDir()
<external-path/>代表的根目錄: Environment.getExternalStorageDirectory()
<cache-path/>代表的根目錄: getCacheDir()
在上述代碼中:
path="."代表的是根目錄丙猬,即你可以向其它的應(yīng)用共享根目錄及其子目錄下任何一個(gè)文件了淮悼。如果使用path="download",那么得到的目錄為“/storage/emulated/0/download”袜腥,只允許向其他應(yīng)用共享download目錄及其子目錄內(nèi)的文件羹令。
在完成以上配置后福侈,使用到的完整的安裝apk的函數(shù)如下:
/**
* 安裝新版本APK
*/
protected void installApk(Activity activity, File file) {
if (activity == null || !file.exists()){
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
// 由于沒(méi)有在Activity環(huán)境下啟動(dòng)Activity,設(shè)置下面的標(biāo)簽
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //aandroid N的權(quán)限問(wèn)題
//賦予臨時(shí)權(quán)限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
//通過(guò)provider生成uri
Uri contentUri = FileProvider.getUriForFile(activity, "com.example.test.fileprovider", file);//注意修改com.example.test為自己的包名
intent.setDataAndType(contentUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}
activity.startActivity(intent);
}
特別需要注意的是:需要注意的是:修改com.example.test為自己的包名。
四伟墙、測(cè)試
- 測(cè)試手機(jī):三星S7 edge
- 服務(wù)器:Tomcat:http://172.26.0.1:8181/updateinfo.json
- 軟件版本:1.0
- 服務(wù)端軟件版本:2.0
- 更改內(nèi)容:主界面新增加一個(gè)textView“這是升級(jí)后的版本”戳葵。
4.1 修改AndroidManifest.xml及build.gradle配置文件
AndroidManifest.xml修改為:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.test"
android:versionName="2.0"
android:versionCode="2">
build.gradle文件修改為:
defaultConfig {
applicationId "com.example.test"
minSdkVersion 19
targetSdkVersion 25
versionCode 2
versionName "2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
4.2 主界面activity_main.xml
新增textView2:“這是升級(jí)后的版本”
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="檢查更新" />
<TextView
android:id="@+id/test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textSize="30sp"
android:textStyle="bold"
android:textColor="@color/colorAccent"
android:layout_centerInParent="true" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/test"
android:textSize="30sp"
android:textStyle="bold"
android:textColor="@color/colorAccent"
android:layout_centerHorizontal="true"
android:layout_marginBottom="49dp"
android:text="這是升級(jí)后的版本" />
</RelativeLayout>
4.3 MainActivity.java
主界面就一個(gè)button用于執(zhí)行版本檢測(cè)
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private int currentVersionCode;
private String currentVersionName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//獲取本地版本號(hào)和版本名
currentVersionCode = VersionGetUtil.getVersionCode(MainActivity.this);
currentVersionName = VersionGetUtil.getVersionName(MainActivity.this);
Log.i(TAG, "onCreate: 版本號(hào):" + currentVersionCode + ",版本名:" + currentVersionName);
final VersionUpdateUtil updateUtil = new VersionUpdateUtil(currentVersionCode,MainActivity.this);
Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Log.i(TAG,"開(kāi)始執(zhí)行版本判斷");
updateUtil.getServerVersionCode();
}
});
TextView textView = (TextView)findViewById(R.id.test);
textView.setText("當(dāng)前版本: " + currentVersionName);
}
}
4.4 測(cè)試
這個(gè)是重中之重,請(qǐng)務(wù)必保持手機(jī)與Tomcat處于同一局域網(wǎng)下邦投,可以采用360隨身wifi尼摹,否則會(huì)出現(xiàn)無(wú)法請(qǐng)求的錯(cuò)誤剂娄,直接提示IO異常阅懦。原因可以參考:Android真機(jī)連接本地部署的Tomcat問(wèn)題耳胎,將最新版本的APK放到Tomcat中怕午,開(kāi)始手機(jī)端的測(cè)試工作郁惜。具體的測(cè)試請(qǐng)參考視頻兆蕉。
五虎韵、總結(jié)
經(jīng)過(guò)連續(xù)幾天的摸索與整理缸废,總算把版本更新的代碼企量、文檔整理完畢梁钾,受益匪淺姆泻。當(dāng)然還存在著很多的不足:
- 沒(méi)有提示移動(dòng)網(wǎng)絡(luò)環(huán)境及wifi環(huán)境的下載提示;
- 沒(méi)有對(duì)動(dòng)態(tài)申請(qǐng)權(quán)限做進(jìn)一步的處理孝凌,如點(diǎn)擊“拒絕”蟀架,“不在詢問(wèn)”片拍,的處理捌省;
- 沒(méi)有進(jìn)行自動(dòng)檢測(cè)版本的設(shè)置纲缓;
- 類的封裝祝高,代碼不夠精簡(jiǎn)工闺;
- SD卡外掛判斷還需要進(jìn)一步的整合斤寂;
知識(shí)的梳理遍搞,語(yǔ)言的整理溪猿,流程化的處理诊县,還需要進(jìn)一步的完善依痊,每天進(jìn)步一點(diǎn)點(diǎn)胸嘁。也希望各位developer共同探討,對(duì)于以上出現(xiàn)的各個(gè)問(wèn)題群井,希望不吝賜教。