1、需求
应用更新是我们必须面对的一个问题,这里主要记录应用内更新,至于应用市场更新咱就不管了,在应用内更新有强制更新和一般的更新,强制更新就是进入应用后的更新提示只能点更新,用户不能进行其他操作。比如:你使用dialog进行更新提示,那这个dialog就只有确认更新按钮,点击外部也不能取消,强迫用户只能点击更新,这就是强制更新的实现方式。
需要特别注意的一点就是在,Android7.0等Android高版本上,由于Android安全机制的限制,私有目录将限制访问,在使用之前的下载安装方法写入APK文件会出异常,我们得加上判断。
2、使用HttpURLConnection 下载并自动安装
参考文章,讲解的非常详细
http://blog.csdn.net/sinat_32526807/article/details/65444944
2.1、工具类AppInnerDownLoder
public class AppInnerDownLoder {
public static String SD_FOLDER = Environment.getExternalStorageDirectory()+"/VersionChecker/";
/**
* 从服务器中下载APK
*/
@SuppressWarnings("unused")
public static void downLoadApk(final Context mContext, final String downURL, final String appName) {
final ProgressDialog pd; // 进度条对话框
pd = new ProgressDialog(mContext);
pd.setCancelable(false);// 必须一直下载完,不可取消
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage("正在下载安装包,请稍后");
pd.setTitle("版本升级");
pd.show();
new Thread() {
@Override
public void run() {
try {
File file = downloadFile(downURL, appName, pd);
sleep(3000);
installApk(mContext, file);
// 结束掉进度条对话框
pd.dismiss();
} catch (Exception e) {
pd.dismiss();
}
}
}.start();
}
/**
* 从服务器下载最新更新文件
*
* @param path 下载路径
* @param pd 进度条
* @return
* @throws Exception
*/
private static File downloadFile(String path, String appName, ProgressDialog pd) throws Exception {
// 如果相等的话表示当前的sdcard挂载在手机上并且是可用的
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
// 获取到文件的大小
int fileSize = conn.getContentLength() / 1024; //KB
pd.setMax(fileSize);
InputStream is = conn.getInputStream();
String fileName = SD_FOLDER + appName + ".apk";
File file = new File(fileName);
try {
// 目录不存在创建目录
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
} catch (Exception e) {
// TODO: handle exception
}
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len;
int total = 0;
while ((len = bis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
total += len;
// 获取当前下载量
pd.setProgress(total/1024);
}
fos.close();
bis.close();
is.close();
return file;
} else {
throw new IOException("未发现有SD卡");
}
}
/**
* 安装apk
*/
private static void installApk(Context mContext, File file) {
Uri fileUri = Uri.fromFile(file);
Intent it = new Intent();
it.setAction(android.content.Intent.ACTION_VIEW);
it.setDataAndType(fileUri, "application/vnd.android.package-archive");
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 防止打不开应用
mContext.startActivity(it);
}
}
2.2、代码调用
AppInnerDownLoder.downLoadApk(MainActivity.this,HttpMethods.Img_URL + bean.getUrl(),"dzbp_update"+bean.getParamValue());
2.3、补充说明
1、如果是按照上面方法中的写入路径Environment.getExternalStorageDirectory(),在Android高版本中是需要运行时权限的,直接运行是会crash的,所以我们需要加上Android版本判断,如果小于23就执行上述方法,如果>23就请求外部存储的读写权限;
这是一般的解决思路,但不是最优的,因为这样是不确定的,如果用户拒绝了读写权限,应用就没办法走更新方法了。
2、我们可以换一种思路来解决这个问题,我们来避免运行时权限检测,
我们可以通过Environment.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)获取路径,该位置的文件是属于应用自己的,在应用卸载时也会随着应用一起被删除掉,并且在使用该文件夹的时候,是不需要SD卡读写权限的 。
关于android的存储相关的信息可以查看谷歌中国的文档,点这里。
3、上述安装方法在7.0上运行时会报 android.os.FileUriExposedException
“私有目录被限制访问”的异常,我们需要重新修改,具体请看 “ 4、应用自动安装,兼容7.0”
3、使用DownloadManager下载并自动安装
参考文章
http://blog.csdn.net/cfy137000/article/details/70257912
3.1、 DownloadManager
DownloadManager是Android提供的用于下载的类,使用起来比较简单,它包含两个静态内部类DownloadManager.Query和DownloadManager.Request;
DownloadManager.Request用来请求一个下载,DownloadManager.Query用来查询下载信息
3.2、 下载APK文件
//使用DownLoadManager来下载
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(apkUrl));
//将文件下载到自己的Download文件夹下,必须是External的
//这是DownloadManager的限制
File file = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test.apk");
request.setDestinationUri(Uri.fromFile(file));
//添加请求 开始下载
long downloadId = mDownloadManager.enqueue(request);
- 说明:Android手机存储包括内部存储(手机运存)、外部存储(机身存储)、扩展存储(TF,SD卡),关于android的存储相关的信息可以查看谷歌中国的文档,点这里。
DownloadManager下载的位置是不能放到内部存储,必须放到Enviroment中,这里建议放到自己应用的文件夹下,也就是通过getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)获取到的路径(不要直接放到SD卡中,需要权限),该位置的文件是属于应用自己的,在应用卸载时也会随着应用一起被删除掉,并且在使用该文件夹的时候,是不需要SD卡读写权限的 。
然后通过request.setDestinationUri来设置存储位置,最后将请求加入到downloadManager中,会获得一个downloadID,这个downloadID比较重要,之后下载状态,进度的查询都靠这个downloadID
3.3、 显示下载进度
需要使用downloadId来查询下载的进度, 就是下载时获取的那个downloadId:
long downloadId = mDownloadManager.enqueue(request);
在查询进度的时候会使用到DownloadManager.Query,在查询的时候,也是使用的Cursor,跟查询数据库是一样的,最后Cursor对象在使用过后要记得关闭。
/**
* 获取进度信息
* @param downloadId 要获取下载的id
* @return 进度信息 max-100
*/
public int getProgress(long downloadId) {
//查询进度
DownloadManager.Query query = new DownloadManager.Query()
.setFilterById(downloadId);
Cursor cursor = null;
int progress = 0;
try {
cursor = mDownloadManager.query(query);//获得游标
if (cursor != null && cursor.moveToFirst()) {
//当前的下载量
int downloadSoFar = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
//文件总大小
int totalBytes = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
progress = (int) (downloadSoFar * 1.0f / totalBytes * 100);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return progress;
}
3.4、 下载完成
下载完成后,DownloadManager会发送一个广播,并且会包含downloadId的信息,这个downloadId就是用来判断是否是我们的下载任务下载完成了。
重写广播接收者:
//下载完成的广播
private class DownloadFinishReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
//下载完成的广播接收者
long completeDownloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
}
}
注册广播接收者:
//注册下载完成的广播
mReceiver = new DownloadFinishReceiver();
registerReceiver(mReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
3.5、 apk解析失败
通过DownloadManager下载的应用,在安装的时候会出现 apk解析失败的提示页面。我当时也很困惑,看了这篇blog后知道是文件读取时的权限问题,就是别人访问不了我们的apk文件。我们需要做的是提升对该文件的读写权限。
/**
* 提升读写权限
* @param filePath 文件路径
* @return
* @throws IOException
*/
public static void setPermission(String filePath) {
String command = "chmod " + "777" + " " + filePath;
Runtime runtime = Runtime.getRuntime();
try {
runtime.exec(command);
} catch (IOException e) {
e.printStackTrace();
}
}
我看这个的时候也是有点mengbi,不过看了博主的解释我就了然了(O(∩_∩)O哈哈~还好我是学过Linux操作系统的o( ̄ヘ ̄o#)),吓的我赶紧翻开我的Linux操作系统教科书看看这条命令。
chmod 是Linux下设置文件权限的命令,后面的三个数字每一个代表不同的用户组
语法为:chmod abc file
其中a,b,c各为一个数字,分别表示User、Group、及Other的权限,
数字为可读(r=4)、可写(w=2)、可执行(x=1)三种权限的组合,
可以组成7种不同的权限,分别用1-7这几个数字代表,
上面的7 = 4 + 2 + 1,就代表该组用户拥有可读,可写,可执行的权限,
上面的命名也可以写成 chmod a=rwx file ,a–表示User、Group、及Other的用户组,rwx–表示可读可写可执行的权限。
关于chmod命令的详细介绍可以看这里
题外话,关于Linux命令,我一般是现用现查,只有用的多才能记住,而不是靠死记,当初为了考试也是不得不记,不过后面不用了也就忘了。所以我们可以啊Q一把,不用记不用记、反正也不用考试O(∩_∩)O哈哈哈~
4、应用自动安装,兼容Android N
4.1、7.0前的安装
就是上面写到的,(2.2、代码调用—安装apk,installApk()方法)
/**
* 安装apk
*/
private static void installApk(Context mContext, File file) {
Uri fileUri = Uri.fromFile(file);
Intent intent = new Intent();
intent .setAction(android.content.Intent.ACTION_VIEW);
intent .setDataAndType(fileUri, "application/vnd.android.package-archive");
intent .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 防止打不开应用
mContext.startActivity(it);
}
4.2、7.0及更高版本的安装
虽然我们把存储安装文件的权限提升为可读可写可执行,但是安装文件所在的应用私有目录还是被限制访问。如果我们在Android7.0及之后的版本上运行上述代码会出现 android.os.FileUriExposedException(“私有目录被限制访问”)
,这是Android系统在Android7.0中为了提高私有文件的安全性,在面向 Android N 或更高版本时访问应用的私有目录将被限制。
这就是7.0的” StrictMode API 政策” 是指禁止向你的应用外公开 file:// URI。 如果一项包含文件 file:// URI类型 的 Intent 离开你的应用,应用失败,并出现 FileUriExposedException 异常。
所以我们需要使用FileProvider来解决,简言之,android7.0禁止应用间文件的直接共享,需要统一使用FileProvider传递URI。谷歌中国文档介绍点击这里。
- 1、定义一个FileProvider
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
属性说明:
1. exported:必须为false
2. grantUriPermissions:true,表示授予 URI 临时访问权限。
3. authorities 组件标识,都以包名开头,避免和其它应用发生冲突。
- 2、指定共享文件的目录
需要在res文件夹中新建xml目录,并且创建file_paths,因为FileProvider的路径必须是先前指定的。
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<paths>
<!--不同存储位置对应的标签也不同-->
<external-path path="" name="download"/>
</paths>
</resources>
path=”“,是有特殊意义的,它代表根目录,也就是说你可以向其它应用共享本应用的根目录及其子目录下任何一个文件了。
不同存储位置对应的标签也不同 , 具体的可以看下面列举
1.应用程序内部存储区域的文件/子目录中的文件Context.getFilesDir() —— <files-path name="name" path="path" />
2.应用程序内部存储区域的缓存子目录中的文件,Context.getCacheDir() —— <cache-path name="name" path="path" />
3.外部存储区域根目录中的文件,Environment.getExternalStorageDirectory() —— <external-path name="name" path="path" />
4.应用程序外部存储区域根目录中的文件 ,Context.getExternalFilesDir() —— <external-files-path name="name" path="path" />
5.应用程序外部缓存区域根目录中的文件,Context.getExternalCacheDir() —— <external-cache-path name="name" path="path" />
我们更新APK文件存储在getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),应用程序外部存储区域根目录下的download文件夹下
- 3、使用FileProvider
File file = (new File(apkPath));
//Uri fileUri = Uri.fromFile(file);
//参数1 上下文, 参数2 Provider主机地址和清单文件中保持一致 参数3 共享的文件
Uri fileUri = FileProvider.getUriForFile(context,"com.mydomain.fileprovider", file);
Intent intent = new Intent();
intent.setAction(android.content.Intent.ACTION_VIEW);
//加上这句,表示对目标应用临时授权(只读)该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);// 防止打不开应用
mContext.startActivity(intent);
相较于之前的代码,这里做了些许调整,
1、是把原来直接的Uri替换成使用FiliProvider创建的Uri,利用FileProvider的getUriForFile方法获取,其中authority参数需要填写清单文件中的authorities的值
2、添加intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)来对目标应用临时授权该Uri所代表的文件
4.3、应用安装的实际代码
加上android版本判断
//应用安装
private static void installNormal(Context context,String apkPath) {
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
//版本在7.0以上是不能直接通过uri访问的
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
File file = (new File(apkPath));
//参数1 上下文, 参数2 Provider主机地址和清单文件中保持一致 参数3 共享的文件
Uri apkUri =
FileProvider.getUriForFile(context, "com.mydomain.fileprovider", file);
//添加这一句表示对目标应用临时授权该Uri所代表的文件
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(new File(apkPath)),
"application/vnd.android.package-archive");
}
// 由于没有在Activity环境下启动Activity,设置下面的标签
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
5、补充、
5.1、安装完成后删除应用
/**
* 删除apk
* @param apkName apk名字
* @return
*/
public static File clearApk(Context context, String apkName) {
File apkFile = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), apkName);
if (apkFile.exists()) {
apkFile.delete();
}
return apkFile;
}