美文网首页Android版本更新Android技术知识Android开发经验谈
你需要知道的版本更新的所有细节!(附Demo)

你需要知道的版本更新的所有细节!(附Demo)

作者: af83084249b7 | 来源:发表于2018-03-20 21:23 被阅读1810次

开始

对于所有的android开发者来说,版本更新几乎是不可能跳过的一环.强如BAT等大厂,虽然很大程度用到了热修复和热更新技术。但是通用的新apk下载,也是必备的一环。

起因

android6.0有了动态权限,android7.0有了privider,android8.0有了notification-channel,各个手机厂商对应用通知的默认差异化处理,安装注意事项等。更新方面还是有很多细节要做的,那么,我这边抽空整合了更新相关的所有细节,方便大家参考。

效果

image

思路

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

相关文章

  • 你需要知道的版本更新的所有细节!(附Demo)

    开始 对于所有的android开发者来说,版本更新几乎是不可能跳过的一环.强如BAT等大厂,虽然很大程度用到了热修...

  • 正则匹配助手更新

    【版本】1.0【版本】【网址】暂无【网址】【更新细节】暂无【更新细节】

  • codepush热更新入门

    简单介绍下如何使用codepush进行热更新,文章尾部附demo地址 1、首先需要继承React-Native环境...

  • 猎杀游戏版本

    【版本】1.0【版本2】【更新细节】暂无更新细节【更新细节2】【下载地址】暂无【下载地址2】【安装包大小】暂无【安...

  • 猎梦快讯app检测更新

    【版本】0.1【版本2】【更新细节】 1、添加检测更新 2、添加下载功能 【更新细节2】【下载地址】http://...

  • swift初级篇

    目录 UITableView swift版本的使用方法(附Demo) StoryBoard使用控制器进行转场传值 ...

  • node同时安装多个版本

    多个 Node.js 版本的安装 Node.js 的版本更新非常快,所有有时需要在多个版本之间切换,就需要安装多个...

  • android版本更新策略

    开发中对版本进行检查并更新的需求基本是所有应用必须有的功能,可是在实际开发中有些朋友就容易忽略一些细节。 版本更新...

  • contos 安装docker

    1 . 更新yum 安装需要的软件包 添加yum源 列出所有的docker版本 安装指定版本的docker 验证安...

  • Android应用内更新,附Demo

    自己写的一个应用内更新小Demo,大家可以参考一下,有问题欢迎提出https://gitee.com/wjboss...

网友评论

  • bruce1990:大神。可以用AsynTask做成在当前页面回调然后展示进度的吗
    bruce1990:@Allen___ 如果在activity做,具体怎么做,大神指导一下。本人还是小白:sob:
    af83084249b7:你这样的话是打算做阻塞式的吗?那不需要service啦,直接当前activity做操作就好了。不推荐asynTask(AsyncTask适用于后台操作只有几秒的短时操作,下载时长跟网络状态有关)
  • 有点健忘:为啥你们都不喜欢用系统自带的下载管理器来下载啊,这样的话通知栏都不用管了,不更方便吗?反正一般上级没要求,我一律用系统的下载
    有点健忘:@Allen___ 所以用系统下载的第一步是先判断下载管理器是否打开的,没有就弹框提示用户去打开。 关闭状态确实有的,最早没加判断的,发现有的手机挂了才注意到,后来都是先判断有没有打开
    af83084249b7:有的手机会报这个错误:Caused by: java.lang.IllegalArgumentException: Unknown URL content://downloads/my_downloads,(可能原因:部分手机的下载管理器是关闭状态)
  • 南山下北海北::smirk: 需要男朋友吗
    af83084249b7:@南山下北海北 需要,来吧😏

本文标题:你需要知道的版本更新的所有细节!(附Demo)

本文链接:https://www.haomeiwen.com/subject/vzriqftx.html