开始
对于所有的android开发者来说,版本更新几乎是不可能跳过的一环.强如BAT等大厂,虽然很大程度用到了热修复和热更新技术。但是通用的新apk下载,也是必备的一环。
起因
android6.0有了动态权限,android7.0有了privider,android8.0有了notification-channel,各个手机厂商对应用通知的默认差异化处理,安装注意事项等。更新方面还是有很多细节要做的,那么,我这边抽空整合了更新相关的所有细节,方便大家参考。
效果
思路
android6.0动态权限相关
首先说下android6.0的权限问题,为了更新专门让用户去开启读写权限,显然不是最佳方案。可以参考大部分主流应用,更新时候要么已经有了读写权限,要么直接执行操作。没有专门去申请动态权限。毕竟这个操作对用户来说也是麻烦了一步,那么,不申请动态权限是怎么做到的呢?
众所周知,安卓基本的文件缓存操作有三大方面:
File file=Environment.getExternalStorageDirectory();
File file1=context.getCacheDir();
File file2=context.getExternalCacheDir();
那么那一个是合适的呢?Environment.getExternalStorageDirectory()显然是不合适的。原因?涉及到动态权限。这个地方的文件读写是需要权限的,所以我们在使用时候必须加上动态权限的申请。于是这个就pass掉了。
那么context.getCacheDir()如何呢?这个当然不需要权限了。因为这是应用本身的内部缓存区域。但是,问题来了。我们可以在这个区域正常写文件,但是我们获取的时候就不行了。为啥呢?我们是开启了intent,去执行相关action。这个intent相关uri指向的文件,对于外部应用来说是无法获取的。所以这个方案也不可行了。
那么,只剩下最后一个方案了。是的,应用的外部缓存区域!我们进行读写操作不需要动态权限,只需manifest配置即可。那么对于intent的uri呢?这个当然没有问题。intent指定的uri是应用的外部缓存区域,所以可以正确执行intent操作。
android7.0的provider相关
对于android7.0的provider主要影响是uri相关。我们不能使用以往的方式Uri.fromFile(File file)去获取文件的uri,这在7.0及以上版本是不支持的。我们这边要判断用户手机版本,然后做差异化获取。
具体流程呢?
首先我们需要在manifest里面配置provider。如下
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.jkt.update.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepath"/>
</provider>
通过上述方式我们指定了authorities(重要,后续会讲)以及resource对应的filepath,
那么filepath又是什么呢?
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_path" path="."/>
<cache-path name="cache_path" path="."/>
</paths>
也就是指明了相关路径等的配置信息,照做就好了。好了,配置完毕了。接下来就是如何使用了。
我这边已经处理了相关代码,主要是判断版本。7.0以下SDK继续使用Uri.fromFile(File file)去获取文件的uri,至于7.0及以上SDK的话,需要用uri = FileProvider.getUriForFile(Context context,String authority,File file)这个方法获取文件的uri。上述provider节点的authorities就是做匹配使用。切记,一定要匹配。
具体代码:
public static Uri getUriForFile(Context context, File file) {
if (context == null || file == null) {
throw new NullPointerException();
}
Uri uri;
if (Build.VERSION.SDK_INT >= 24) {
uri = FileProvider.getUriForFile(context.getApplicationContext(), "com.jkt.update.FileProvider", file);
} else {
uri = Uri.fromFile(file);
}
return uri;
}
android8.0通知渠道
相比较之前的通知,8.0多了通知渠道这个概念。那么如果处理呢?主要是两个点。一是notification增加channelId,二是notificationManager新增channelId如下:
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
//大于等于0版本,构造对象添加channelId
builder = new NotificationCompat.Builder(getApplicationContext(), mChannelId);
NotificationChannel channel = new NotificationChannel(mChannelId,
getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
//创建channelId
mNotificationManager.createNotificationChannel(channel);
} else {
builder = new NotificationCompat.Builder(getApplicationContext());
}
多机型通知处理
国产手机厂商对andorid修改幅度还是不小的.通知这块就比较大.对于不同的机型,默认通知权限是不同的.我们在更新的时候要么前台dialog通知进度,要么开启通知告知用户进度.前台dialog形式明显不是特别理想化,毕竟阻塞用户交互,不是特别优化.那么通知的dialog就是不错的选择.
所以我们选择通知的方式告知用户进度,那么并不是如此简单.我们需要获取用户当前app是否有通知权限.如果没有的话,最好还是对话框提醒用户.当然.这个提醒我做的是三个选择.取消、确认、跳过.当用户点击确认则会进入当前app的管理页面,用户可以主动开启通知.对于不想处理这些细节的用户,可以选择跳过.直接执行下载以及后续更新功能.但是,不会收到进度通知.
不过,这种情况下用户已经知晓,所以不会存在,无感知状态的不好体验.
安装处理
最后的安装动作,是需要添加uri的读写权限的,以及开启新的任务栈的。
如下:
public Intent getFileIntent(File file) {
Uri uri = UriUtil.getUriForFile(getBaseContext(), file);
String type = getMIMEType(file);
Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.DEFAULT");
//下面的flags不添加,在部分手机会安装失败
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, type);
return intent;
}
流程
第一步:更新检测之后对话框提示
//首先点击提示更新相关信息(实际情况点击事件应该换成更新数据接口事件响应)
public void bnClick(View view) {
AlertDialog.Builder dialog =
new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("版本更新");
dialog.setMessage("更新至新的版本");
dialog.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//判断通知权限的有无,后续分状态处理
beforeUpdateWork();
}
});
dialog.setNegativeButton("关闭",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//...To-do
}
});
dialog.show();
}
第二步:判断是否授予了通知权限,分逻辑处理
private void beforeUpdateWork() {
//没有权限的话,新对话框提醒用户
if (!isEnableNotification()) {
showNotificationAsk();
return;
}
toIntentServiceUpdate();
}
private void showNotificationAsk() {
AlertDialog.Builder dialog =
new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("通知权限");
dialog.setMessage("打开通知权限");
dialog.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//打开权限页面,让用户开启通知权限
toSetting();
}
});
dialog.setNeutralButton("跳过", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//用户可以跳过直接执行下载动作,但是不会有通知进度提醒(不过用户已知晓)
toIntentServiceUpdate();
}
});
dialog.setNegativeButton("关闭",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//...To-do
}
});
dialog.show();
}
第三步:启动IntentService,进行下载.以及后续安装
private void toIntentServiceUpdate() {
//里边用的是原生网络处理.包含进度获取.更新通知.最后下载完毕,开启安装页面等.
Intent updateIntent = new Intent(this, UpdateIntentService.class);
updateIntent.setAction(UpdateIntentService.ACTION_UPDATE);
updateIntent.putExtra("appName", "update-1.0.1");
//随便一个apk的url进行模拟
updateIntent.putExtra("downUrl", "http://gdown.baidu.com/data/wisegame/38cbb321c273886e/YY_30086.apk");
startService(updateIntent);
}
核心代码(推荐大家下载Demo更方便)
///UpdateIntentService
public class UpdateIntentService extends IntentService {
public static final String ACTION_UPDATE = "com.jkt.update.action.UPDATE";
private NotificationManager mNotificationManager;
private RemoteViews mRemoteViews;
private Notification mNotification;
private Handler mUpdateHandler;
private String mChannelId = "updateChannel";
public UpdateIntentService() {
super("MyIntentService");
}
/**
* Starts this service to perform action Foo with the given parameters. If
* the service is already performing a task this action will be queued.
*
* @see IntentService
*/
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
switch (action) {
case UpdateIntentService.ACTION_UPDATE:
handleActionUpdate(intent);
break;
default:
break;
}
}
}
private void handleActionUpdate(Intent intent) {
getUpdateHandler();
beforeUpdateMessage();
File file = updateIo(intent);
finishUpdateMessage(file);
}
private void getUpdateHandler() {
mUpdateHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.arg1) {
case 0:
createNotification();
//start
break;
case 1:
updateNotification(msg);
//updateingMessage
break;
case 2:
installApk(msg);
//finish
case 3:
//error
installApk(msg);
break;
}
return true;
}
});
}
private File updateIo(Intent intent) {
File updateFile = FileUtil.getDiskCacheDir(getApplicationContext(), intent.getStringExtra("name") + System.currentTimeMillis() + ".apk");
try {
URL url = new URL(intent.getStringExtra("downUrl"));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000);
conn.setRequestProperty("Accept-Encoding", "identity");
conn.connect();
int length = conn.getContentLength();
InputStream inputStream = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(updateFile, true);
int oldProgress = 0;
byte buf[] = new byte[1024 * 8];
int currentLength = 0;
while (true) {
int num = inputStream.read(buf);
currentLength += num;
// 计算进度条位置
int progress = (int) ((currentLength / (float) length) * 100);
if (progress > oldProgress) {
updatingMessage(progress);
oldProgress = progress;
}
if (num <= 0) {
break;
}
fos.write(buf, 0, num);
fos.flush();
}
fos.flush();
fos.close();
inputStream.close();
} catch (Exception e) {
Log.i("updateException", e.toString());
return null;
}
return updateFile;
}
private void beforeUpdateMessage() {
Message message = mUpdateHandler.obtainMessage();
message.arg1 = 0;
mUpdateHandler.sendMessage(message);
}
private void updatingMessage(int progress) {
Message message = mUpdateHandler.obtainMessage();
message.arg1 = 1;
message.obj = progress;
mUpdateHandler.sendMessage(message);
}
private void finishUpdateMessage(File file) {
Message message = mUpdateHandler.obtainMessage();
message.arg1 = 2;
message.obj = file;
mUpdateHandler.sendMessage(message);
}
public void createNotification() {
mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = null;
//notification channel work
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
builder = new NotificationCompat.Builder(getApplicationContext(), mChannelId);
NotificationChannel channel = new NotificationChannel(mChannelId,
getString(R.string.app_name), NotificationManager.IMPORTANCE_DEFAULT);
mNotificationManager.createNotificationChannel(channel);
} else {
builder = new NotificationCompat.Builder(getApplicationContext());
}
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setTicker("开始下载");
mRemoteViews = new RemoteViews(getPackageName(), R.layout.notification_update);
mRemoteViews.setProgressBar(R.id.notificationProgress, 100, 0, false);
builder.setCustomContentView(mRemoteViews);
mNotification = builder.build();
//Notification.FLAG_ONLY_ALERT_ONCE 避免8.0在进度更新时候(notify)中多次响铃
//Notification.FLAG_NO_CLEAR 下载过程中无法关闭通知,失败或者完成会切换到可以关闭
mNotification.flags = Notification.FLAG_NO_CLEAR|Notification.FLAG_ONLY_ALERT_ONCE;
mNotification.icon = R.mipmap.ic_launcher;
mNotificationManager.notify(0, mNotification);
}
private void updateNotification(Message msg) {
Integer aFloat = (Integer) msg.obj;
mRemoteViews.setProgressBar(R.id.notificationProgress, 100, aFloat, false);
mRemoteViews.setTextViewText(R.id.notification_note_tv, "正在下载 " + aFloat + "%");
mNotificationManager.notify(0, mNotification);
}
// 下载完成后打开安装apk界面
public void installApk(Message msg) {
File file = (File) msg.obj;
if (file == null || file.length() == 0) {
mRemoteViews.setTextViewText(R.id.notification_note_tv, "下载失败 ");
//下载失败,flags设置为可关闭
mNotification.flags = Notification.FLAG_AUTO_CANCEL;
mNotificationManager.notify(0, mNotification);
return;
}
//关闭之前的通知,为了兼容某些手机在mNotification.contentIntent后不更新的bug
//,保证PendingIntent正确执行
mNotificationManager.cancel(0);
mRemoteViews.setProgressBar(R.id.notificationProgress, 100, 100, false);
mRemoteViews.setTextViewText(R.id.notification_note_tv, "下载完毕 ");
//下载完成,flags设置为可关闭
mNotification.flags = Notification.FLAG_AUTO_CANCEL|Notification.FLAG_ONLY_ALERT_ONCE;
Intent openFile = getFileIntent(file);
mNotification.contentIntent = PendingIntent.getActivity(this, 0, openFile, 0);
mNotificationManager.notify(1, mNotification);
startActivity(openFile);
}
public Intent getFileIntent(File file) {
Uri uri = UriUtil.getUriForFile(getBaseContext(), file);
String type = getMIMEType(file);
Intent intent = new Intent("android.intent.action.VIEW");
intent.addCategory("android.intent.category.DEFAULT");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(uri, type);
return intent;
}
public String getMIMEType(File file) {
String type = null;
String suffix = file.getName().substring(file.getName().lastIndexOf(".") + 1, file.getName().length());
if (suffix.equals("apk")) {
type = "application/vnd.android.package-archive";
} else {
// /*如果无法直接打开,就跳出软件列表给用户选择 */
type = "*/*";
}
return type;
}
}
总结
对于更新部分五大块以及流程,上述已经细致讲过了。还有疑问的童鞋可以评论留言。我这边留下仓库地址,方便大家交流以及移植代码。
地址: https://github.com/HoldMyOwn/Update
网友评论