Android 天气APP(二十六)增加自动更新(检查版本、通知栏下载、自动安装)

简介: Android 天气APP(二十六)增加自动更新(检查版本、通知栏下载、自动安装)

效果图


20200901093220571.gif


开发流程



1.开发前言


 对于任何APP来说基本上都会有自动更新这个功能,那么为什么我的这个APP之前没有写呢?因为之前APP比较小,更像是一个Demo,看的人没有几个,更像是我在自娱自乐,不过随着我不断的更新APP的功能和写博客,使用的用户增加了,虽然不多,但我还是蛮开心的,增加APP自动更新这个功能的好处就是,无论以后我更新了什么功能,只要你打开APP就会提醒你新增的功能,你可以安装使用,也可以不安装都随你。说了这么多废话了,还是来实践吧。


2.上传应用到分发平台


 作为个体开发者,我的数据都来源于网络API,没有自己的服务器和数据库,而且我也没有上架到应用市场,因为现在个人开发者上架应用市场需要的东西比较多,一时半会也搞不了,所以退而求其次,使用分发平台,这个平台首先是免费的,其次比较方便去测试,常规的就是蒲公英、闪红、Fir.im等一些,我这里用的是Fir.im,可以用邮箱注册,然后实名认证通过之后,你就可以上传应用上去了。那些步骤都属于基本操作,我就不过多赘述,重点来看版本更新的接口。往下进行时,请先确保平台上有你上传得APK才行。


点击应用检测更新接口

实际上我需要的只是id和token


20200831165156696.png


先来看id,点击这个应用的图标,然后点击基本信息,下面就会看到应用id


20200831165318515.png


再来看api_token,鼠标悬停在你的用户名上面,选择API token


20200831165458929.png


如果你是第一次使用,那么会让你先生成一个随机的API Token,复制即可,记住不要乱改。


20200831165546299.png


现在该有的都有了。我们看看它的返回示例是怎么样的。这里我就把我的更新API接口放到这里,


http://api.bq04.com/apps/latest/5e8c37e90d81cc0db2645c1c?api_token=468e4653ca9e1d34e7a73b8f2d7191da


到浏览器打开,你会看到这样的数据


20200831165845158.png


然后再格式化一下


{
  "name": "好天气",
  "version": "1",
  "changelog": "新增自动更新",
  "updated_at": 1598861611,
  "versionShort": "2.2",
  "build": "1",
  "installUrl": "https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb\u0026source=update",
  "install_url": "https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb\u0026source=update",
  "direct_install_url": "https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb\u0026source=update",
  "update_url": "http://jappstore.com/h4sg",
  "binary": {
    "fsize": 31071757
  }
}


3.版本数据请求与存储


可以看到该有的数据都有,OK,现在先来生成这个实体。这里我会把数据放到数据库里,方便随时调用。所以在mvplibrary下面的bean包下新建一个AppVersion,继承 LitePalSupport


2020083117041043.png


代码如下:


package com.llw.mvplibrary.bean;
import org.litepal.crud.LitePalSupport;
/**
 * 应用版本更新
 */
public class AppVersion extends LitePalSupport {
    /**
     * name : 好天气
     * version : 1
     * changelog : 新增地图天气
     * updated_at : 1598581575
     * versionShort : 2.1
     * build : 1
     * installUrl : https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb&source=update
     * install_url : https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb&source=update
     * direct_install_url : https://download.jappstore.com/apps/5e8c37e90d81cc0db2645c1c/install?download_token=dccf478d29848e406f371890ac761bfb&source=update
     * update_url : http://jappstore.com/h4sg
     * binary : {"fsize":31038590}
     */
    private String name;
    private String version;
    private String changelog;
    private int updated_at;
    private String versionShort;
    private String build;
    private String installUrl;
    private String install_url;
    private String direct_install_url;
    private String update_url;
    private String appSize;
    public String getAppSize() {
        return appSize;
    }
    public void setAppSize(String appSize) {
        this.appSize = appSize;
    }
    private BinaryBean binary;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getVersion() {
        return version;
    }
    public void setVersion(String version) {
        this.version = version;
    }
    public String getChangelog() {
        return changelog;
    }
    public void setChangelog(String changelog) {
        this.changelog = changelog;
    }
    public int getUpdated_at() {
        return updated_at;
    }
    public void setUpdated_at(int updated_at) {
        this.updated_at = updated_at;
    }
    public String getVersionShort() {
        return versionShort;
    }
    public void setVersionShort(String versionShort) {
        this.versionShort = versionShort;
    }
    public String getBuild() {
        return build;
    }
    public void setBuild(String build) {
        this.build = build;
    }
    public String getInstallUrl() {
        return installUrl;
    }
    public void setInstallUrl(String installUrl) {
        this.installUrl = installUrl;
    }
    public String getInstall_url() {
        return install_url;
    }
    public void setInstall_url(String install_url) {
        this.install_url = install_url;
    }
    public String getDirect_install_url() {
        return direct_install_url;
    }
    public void setDirect_install_url(String direct_install_url) {
        this.direct_install_url = direct_install_url;
    }
    public String getUpdate_url() {
        return update_url;
    }
    public void setUpdate_url(String update_url) {
        this.update_url = update_url;
    }
    public BinaryBean getBinary() {
        return binary;
    }
    public void setBinary(BinaryBean binary) {
        this.binary = binary;
    }
    public static class BinaryBean {
        /**
         * fsize : 31038590
         */
        private int fsize;
        public int getFsize() {
            return fsize;
        }
        public void setFsize(int fsize) {
            this.fsize = fsize;
        }
    }
}


然后改动一下litepal.xml配置文件


20200831170546628.png


数据表有了,下面就是要请求数据了,所以要新增一个API接口,打开ServiceGenerator,新增如下,我将之前的测试地址做了一个拆分,这样规范管理,虽说有些麻烦。


      case 5://APP更新
                BASE_URL = "http://api.bq04.com";//Fr.im更新
                break;

20200831170747904.png



然后打开ApiService,新增接口方法,因为里面的id和api token是固定的所以我就不用动态传递过去了,直接写死在url里面,当然如果你要写的话记得要用自己的id和api token,用我的你是那不到返回数据的。


  /**
     * APP版本更新
     */
    @GET("/apps/latest/你的id?api_token=你的API Token")
    Call<AppVersion> getAppInfo();

20200831171000167.png


那么下面就要写一个订阅器了,我的想法是在应用欢迎页就请求数据。那么就在app下的contract包下创建一个SplashContract,里面的代码如下:


package com.llw.goodweather.contract;
import com.llw.goodweather.api.ApiService;
import com.llw.goodweather.bean.NewSearchCityResponse;
import com.llw.goodweather.bean.NowResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.bean.AppVersion;
import com.llw.mvplibrary.net.NetCallBack;
import com.llw.mvplibrary.net.ServiceGenerator;
import retrofit2.Call;
import retrofit2.Response;
/**
 * 欢迎页订阅器
 */
public class SplashContract {
    public static class SplashPresenter extends BasePresenter<ISplashView> {
        /**
         * 获取最新的APP版本信息
         */
        public void getAppInfo() {//注意这里的4表示新的搜索城市地址接口
            ApiService service = ServiceGenerator.createService(ApiService.class, 5);
            service.getAppInfo().enqueue(new NetCallBack<AppVersion>() {
                @Override
                public void onSuccess(Call<AppVersion> call, Response<AppVersion> response) {
                    if(getView() != null){
                        getView().getAppInfoResult(response);
                    }
                }
                @Override
                public void onFailed() {
                    if(getView() != null){
                        getView().getDataFailed();
                    }
                }
            });
        }
    }
    public interface ISplashView extends BaseView {
        //APP信息返回
        void getAppInfoResult(Response<AppVersion> response);
        //错误返回
        void getDataFailed();
    }
}


都是属于基本操作了,不需要解释了。


然后打开SplashActivity


2020083117145595.png


增加请求


20200831171703194.png


实现如下三个方法


  @Override
    protected SplashContract.SplashPresenter createPresent() {
        return new SplashContract.SplashPresenter();
    }
    /**
     * 获取APP最新版本信息返回
     * @param response
     */
    @Override
    public void getAppInfoResult(Response<AppVersion> response) {
        if (response.body() != null) {
            AppVersion appVersion = new AppVersion();
            appVersion.setName(response.body().getName());//应用名称
            appVersion.setVersion(response.body().getVersion());//应用版本 对应code
            appVersion.setVersionShort(response.body().getVersionShort());//应用版本名
            appVersion.setChangelog(response.body().getChangelog());//更新日志
            appVersion.setUpdate_url(response.body().getUpdate_url());//更新地址
            appVersion.setInstall_url(response.body().getInstall_url());//安装地址
            appVersion.setAppSize(String.valueOf(response.body().getBinary().getFsize()));//APK大小
            //添加数据前先判断是否已经有数据了
            if (LitePal.find(AppVersion.class, 1) != null){
                appVersion.update(1);//更新数据
            }else {
                appVersion.save();//保存添加数据
            }
        }
    }
    @Override
    public void getDataFailed() {
        Log.d("Network Error","网络异常");
    }


现在这个数据就已经有了并且也储存起来了,下一步就是检测这个版本的更新了。


4.检查版本更新、自定义更新提示弹窗


这里我写了两个工具类,代码如下


APKVersionInfoUtils .java


package com.llw.goodweather.utils;
import android.content.Context;
import android.content.pm.PackageManager;
/**
 * Created by chenxi on 2019/4/29.
 * APP版本信息获取工具类
 */
public class APKVersionInfoUtils {
    /**
     * 获取当前本地apk的版本
     *
     * @param mContext
     * @return
     */
    public static int getVersionCode(Context mContext) {
        int versionCode = 0;
        try {
            //获取软件版本号,对应AndroidManifest.xml下android:versionCode
            versionCode = mContext.getPackageManager().
                    getPackageInfo(mContext.getPackageName(), 0).versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return versionCode;
    }
    /**
     * 获取版本号名称
     *
     * @param context 上下文
     * @return
     */
    public static String getVerName(Context context) {
        String verName = "";
        try {
            verName = context.getPackageManager().
                    getPackageInfo(context.getPackageName(), 0).versionName;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return verName;
    }
}


AppStartUpUtils .java


package com.llw.goodweather.utils;
import android.content.Context;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * APP启动判断工具类
 */
public class AppStartUpUtils {
    /**
     * 判断是否是首次启动
     * @param context
     * @return
     */
    public static boolean isFirstStartApp(Context context) {
        Boolean isFirst = SPUtils.getBoolean(Constant.APP_FIRST_START, true, context);
        if (isFirst) {// 第一次
            SPUtils.putBoolean(Constant.APP_FIRST_START, false, context);
            return true;
        } else {
            return false;
        }
    }
    /**
     * 判断是否是今日首次启动APP
     * @param context
     * @return
     */
    public static boolean isTodayFirstStartApp(Context context) {
        String saveDate = SPUtils.getString(Constant.START_UP_APP_TIME, "2020-08-27", context);
        String todayDate = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        if (!saveDate.equals(todayDate)) {//第一次打开
            SPUtils.putString(Constant.START_UP_APP_TIME, todayDate, context);
            return true;
        } else {
            return false;
        }
    }
}


第二个工具类里面用到了一些全局的参数,如下:


package com.llw.goodweather.utils;
/**
 * 统一管理缓存中对应的KEY
 */
public class Constant {
    public final static String SUCCESS_CODE = "200";
    public final static String CITY = "city";//市
    public final static String DISTRICT = "district";//区/县
    public final static String LOCATION_ID = "locationId";//通过搜索接口得到的城市id,在V7中所有数据通过id来查询
    public final static String EVERYDAY_IMG = "everyday_img";//每日图片开关
    public final static String IMG_LIST = "img_list";//图片列表开关
    public final static String CUSTOM_IMG = "custom_img";//手动定义开关
    public final static String IMG_POSITION = "img_position";//选中的本地背景图片
    public final static  int SELECT_PHOTO = 2;//启动相册标识
    public final static String CUSTOM_IMG_PATH = "custom_img_path";//手动上传图片地址
    public final static String FLAG_OTHER_RETURN="flag_other_return";//跳转页面的标识
    public final static String LOCATION="location";
    public final static String APP_FIRST_START = "appFirstStart";//App首次启动
    public final static String START_UP_APP_TIME = "startAppTime";//今日启动APP的时间
}


然后在MainActivity中就可以进行判断了


  /**
     * 检查APP版本
     */
    private void checkAppVersion() {
        AppVersion appVersion = LitePal.find(AppVersion.class,1);
        if(!appVersion.getVersionShort().equals(APKVersionInfoUtils.getVerName(context))){//提示更新
            if(AppStartUpUtils.isTodayFirstStartApp(context)){//今天第一次打开APP
                //更新提示弹窗
            }
        }
    }


下面来写这个更新提示弹窗,我就把我好久之前写的放上来了。

首先增加样式文件,在mvplibrary下面的styles.xml中


20200831172819576.png


  <!--自定义对话框-->
    <style name="dialog" parent="@android:style/Theme.Dialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:backgroundDimEnabled">true</item>
        <item name="android:windowNoTitle">true</item>
    </style>
    <!--对话框弹出和消失动画-->
    <style name="dialog_from_bottom_anim">
        <item name="android:windowEnterAnimation">@anim/dialog_from_bottom_anim_in</item>
        <item name="android:windowExitAnimation">@anim/dialog_from_bottom_anim_out</item>
    </style>
    <style name="dialog_from_top_anim">
        <item name="android:windowEnterAnimation">@anim/dialog_from_top_anim_in</item>
        <item name="android:windowExitAnimation">@anim/dialog_from_top_anim_out</item>
    </style>
    <style name="dialog_scale_anim">
        <item name="android:windowEnterAnimation">@anim/dialog_scale_anim_in</item>
        <item name="android:windowExitAnimation">@anim/dialog_scale_anim_out</item>
    </style>


里面还有6个动画文件。如下:

在anim下创建

dialog_from_bottom_anim_in.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="400"
        android:fromXDelta="0"
        android:fromYDelta="1000"
        android:toXDelta="0"
        android:toYDelta="0" />
</set>


dialog_from_bottom_anim_out.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="400"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="1000" />
</set>


dialog_from_top_anim_in.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="1000"
        android:fromYDelta="-100%"
        android:toYDelta="0" />
</set>


dialog_from_top_anim_out.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <translate
        android:duration="1000"
        android:fromYDelta="0"
        android:toYDelta="-100%" />
</set>


dialog_scale_anim_in.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <scale
        android:duration="135"
        android:fromXScale="0.8"
        android:fromYScale="0.8"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="1.05"
        android:toYScale="1.05" />
    <scale
        android:duration="105"
        android:fromXScale="1.05"
        android:fromYScale="1.05"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="135"
        android:toXScale="0.95"
        android:toYScale="0.95" />
    <scale
        android:duration="60"
        android:fromXScale="0.95"
        android:fromYScale="0.95"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="240"
        android:toXScale="1.0"
        android:toYScale="1.0" />
    <alpha
        android:duration="90"
        android:fromAlpha="0.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="1.0" />
</set>


dialog_scale_anim_out.xml


<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
    <scale
        android:duration="150"
        android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="0.6"
        android:toYScale="0.6" />
    <alpha
        android:duration="150"
        android:fromAlpha="1.0"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:toAlpha="0.0" /> 
</set>


下面就是关于自定义Dialog的内容了。


2020083117332375.png


在view下新增一个dialog文件夹,然后一一创建下面的文件


AlertDialog.java


package com.llw.mvplibrary.view.dialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import com.llw.mvplibrary.R;
/**
 * 自定义弹窗
 */
public class AlertDialog extends Dialog {
    private AlertController mAlert;
    public AlertDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);
        mAlert = new AlertController(this, getWindow());
    }
    public void setText(int viewId, CharSequence text) {
        mAlert.setText(viewId, text);
    }
    public <T extends View> T getView(int viewId) {
        return mAlert.getView(viewId);
    }
    public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
        mAlert.setOnClickListener(viewId, onClickListener);
    }
//----------------------------------------------------------------------------------------------
    public static class Builder {
        private final AlertController.AlertParams P;
        public Builder(Context context) {
            this(context, R.style.dialog);
        }
        public Builder(Context context, int themeResId) {
            P = new AlertController.AlertParams(context, themeResId);
        }
        /**
         * 设置对话框布局
         *
         * @param view
         * @return
         */
        public Builder setContentView(View view) {
            P.mView = view;
            P.mLayoutResId = 0;
            return this;
        }
        /**
         * @param layoutId
         * @return
         */
        public Builder setContentView(int layoutId) {
            P.mView = null;
            P.mLayoutResId = layoutId;
            return this;
        }
        /**
         * 设置文本
         *
         * @param viewId
         * @param text
         * @return
         */
        public Builder setText(int viewId, CharSequence text) {
            P.mTextArray.put(viewId, text);
            return this;
        }
        /**
         * 设置文本颜色
         *
         * @param viewId
         * @param color
         * @return
         */
        public Builder setTextColor(int viewId, int color) {
            P.mTextColorArray.put(viewId, color);
            return this;
        }
        /**
         * 设置图标
         *
         * @param iconId
         * @return
         */
        public Builder setIcon(int iconId, int resId) {
            P.mIconArray.put(iconId, resId);
            return this;
        }
        /**
         * 设置图片
         *
         * @param viewId
         * @return
         */
        public Builder setBitmap(int viewId, Bitmap bitmap) {
            P.mBitmapArray.put(viewId, bitmap);
            return this;
        }
        /**
         * 设置对话框宽度占满屏幕
         *
         * @return
         */
        public Builder fullWidth() {
            P.mWidth = ViewGroup.LayoutParams.MATCH_PARENT;
            return this;
        }
        /**
         * 对话框底部弹出
         *
         * @param isAnimation
         * @return
         */
        public Builder fromBottom(boolean isAnimation) {
            if (isAnimation) {
                P.mAnimation = R.style.dialog_from_bottom_anim;
            }
            P.mGravity = Gravity.BOTTOM;
            return this;
        }
        /**
         * 对话框右部弹出
         *
         * @param isAnimation
         * @return
         */
        public Builder fromRight(boolean isAnimation) {
            if (isAnimation) {
                P.mAnimation = R.style.dialog_from_bottom_anim;
            }
            P.mGravity = Gravity.RIGHT;
            return this;
        }
        /**
         * 设置对话框宽高
         *
         * @param width
         * @param heigth
         * @return
         */
        public Builder setWidthAndHeight(int width, int heigth) {
            P.mWidth = width;
            P.mHeight = heigth;
            return this;
        }
        /**
         * 设置对话框宽高
         *
         * @param width
         * @param heigth
         * @return
         */
        public Builder setWidthAndHeightMargin(int width, int heigth, int heightMargin, int widthMargin) {
            P.mWidth = width;
            P.mHeight = heigth;
            P.mHeightMargin = heightMargin;
            P.mWidthMargin = widthMargin;
            return this;
        }
        /**
         * 添加默认动画
         *
         * @return
         */
        public Builder addDefaultAnimation() {
            P.mAnimation = R.style.dialog_scale_anim;
            return this;
        }
        /**
         * 设置动画
         *
         * @param styleAnimation
         * @return
         */
        public Builder setAnimation(int styleAnimation) {
            P.mAnimation = styleAnimation;
            return this;
        }
        /**
         * 设置点击事件
         *
         * @param viewId
         * @param onClickListener
         * @return
         */
        public Builder setOnClickListener(int viewId, View.OnClickListener onClickListener) {
            P.mClickArray.put(viewId, onClickListener);
            return this;
        }
        public Builder setOnLongClickListener(int viewId, View.OnLongClickListener onLongClickListener) {
            P.mLondClickArray.put(viewId, onLongClickListener);
            return this;
        }
        /**
         * Sets whether the dialog is cancelable or not.  Default is true.
         *
         * @return This Builder object to allow for chaining of calls to set methods
         */
        public Builder setCancelable(boolean cancelable) {
            P.mCancelable = cancelable;
            return this;
        }
        public Builder setOnCancelListener(OnCancelListener onCancelListener) {
            P.mOnCancelListener = onCancelListener;
            return this;
        }
        public Builder setOnDismissListener(OnDismissListener onDismissListener) {
            P.mOnDismissListener = onDismissListener;
            return this;
        }
        public Builder setOnKeyListener(OnKeyListener onKeyListener) {
            P.mOnKeyListener = onKeyListener;
            return this;
        }
        public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, P.mThemeResId);
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }
        public AlertDialog show() {
            final AlertDialog dialog = create();
            dialog.show();
            return dialog;
        }
    }
}


AlertController.java

package com.llw.mvplibrary.view.dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
/**
 * 弹窗控制
 */
public class AlertController {
    private AlertDialog mAlertDialog;
    private Window mWindow;
    private DialogViewHelper mViewHelper;
    public AlertController(AlertDialog alertDialog, Window window) {
        mAlertDialog = alertDialog;
        mWindow = window;
    }
    public void setDialogViewHelper(DialogViewHelper dialogViewHelper) {
        mViewHelper = dialogViewHelper;
    }
    public void setText(int viewId, CharSequence text) {
        mViewHelper.setText(viewId, text);
    }
    public void setIcon(int viewId, int resId) {
        mViewHelper.setIcon(viewId, resId);
    }
    public <T extends View> T getView(int viewId) {
        return mViewHelper.getView(viewId);
    }
    public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
        mViewHelper.setOnClickListener(viewId, onClickListener);
    }
    public AlertDialog getDialog() {
        return mAlertDialog;
    }
    public Window getWindow() {
        return mWindow;
    }
//-------------------------------------------------------------------------------------------------
    public static class AlertParams {
        public Context mContext;
        //对话框主题背景
        public int mThemeResId;
        public boolean mCancelable;
        public DialogInterface.OnCancelListener mOnCancelListener;
        public DialogInterface.OnDismissListener mOnDismissListener;
        public DialogInterface.OnKeyListener mOnKeyListener;
        //文本颜色
        public SparseArray<Integer> mTextColorArray = new SparseArray<>();
        //存放文本的更改
        public SparseArray<CharSequence> mTextArray = new SparseArray<>();
        //存放点击事件
        public SparseArray<View.OnClickListener> mClickArray = new SparseArray<>();
        //存放长按点击事件
        public SparseArray<View.OnLongClickListener> mLondClickArray = new SparseArray<>();
        //存放对话框图标
        public SparseArray<Integer> mIconArray = new SparseArray<>();
        //存放对话框图片
        public SparseArray<Bitmap> mBitmapArray = new SparseArray<>();
        //对话框布局资源id
        public int mLayoutResId;
        //对话框的view
        public View mView;
        //对话框宽度
        public int mWidth;
        //对话框高度
        public int mHeight;
        //对话框垂直外边距
        public int mHeightMargin;
        //对话框横向外边距
        public int mWidthMargin;
        //动画
        public int mAnimation;
        //对话框显示位置
        public int mGravity = Gravity.CENTER;
        public AlertParams(Context context, int themeResId) {
            mContext = context;
            mThemeResId = themeResId;
        }
        public void apply(AlertController alert) {
            //设置对话框布局
            DialogViewHelper dialogViewHelper = null;
            if (mLayoutResId != 0) {
                dialogViewHelper = new DialogViewHelper(mContext, mLayoutResId);
            }
            if (mView != null) {
                dialogViewHelper = new DialogViewHelper();
                dialogViewHelper.setContentView(mView);
            }
            if (dialogViewHelper == null) {
                throw new IllegalArgumentException("please set layout");
            }
            //将对话框布局设置到对话框
            alert.getDialog().setContentView(dialogViewHelper.getContentView());
            //设置DialogViewHelper辅助类
            alert.setDialogViewHelper(dialogViewHelper);
            //设置文本
            for (int i = 0; i < mTextArray.size(); i++) {
                alert.setText(mTextArray.keyAt(i), mTextArray.valueAt(i));
            }
            //设置图标
            for (int i = 0; i < mIconArray.size(); i++) {
                alert.setIcon(mIconArray.keyAt(i), mIconArray.valueAt(i));
            }
            //设置点击
            for (int i = 0; i < mClickArray.size(); i++) {
                alert.setOnClickListener(mClickArray.keyAt(i), mClickArray.valueAt(i));
            }
            //配置自定义效果,底部弹出,宽高,动画,全屏
            Window window = alert.getWindow();
            window.setGravity(mGravity);//显示位置
            if (mAnimation != 0) {
                window.setWindowAnimations(mAnimation);//设置动画
            }
            //设置宽高
            WindowManager.LayoutParams params = window.getAttributes();
            params.width = mWidth;
            params.height = mHeight;
            params.verticalMargin = mHeightMargin;
            params.horizontalMargin = mWidthMargin;
            window.setAttributes(params);
        }
    }
}


DialogViewHelper.java

package com.llw.mvplibrary.view.dialog;
import android.content.Context;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.ref.WeakReference;
public class DialogViewHelper {
    private View mContentView;
    private SparseArray<WeakReference<View>> mViews;
    public DialogViewHelper(Context context, int layoutResId) {
        this();
        mContentView = LayoutInflater.from(context).inflate(layoutResId, null);
    }
    public DialogViewHelper() {
        mViews = new SparseArray<>();
    }
    public <T extends View> void setText(int viewId, CharSequence text) {
        TextView tv = getView(viewId);
        if (tv != null) {
            tv.setText(text);
        }
    }
    public <T extends View> T getView(int viewId) {
        WeakReference<View> weakReference = mViews.get(viewId);
        View view = null;
        if (weakReference != null) {
            view = weakReference.get();
        }
        if (view == null) {
            view = mContentView.findViewById(viewId);
            if (view != null) {
                mViews.put(viewId, new WeakReference<>(view));
            }
        }
        return (T) view;
    }
    public void setOnClickListener(int viewId, View.OnClickListener onClickListener) {
        View view = getView(viewId);
        if (view != null) {
            view.setOnClickListener(onClickListener);
        }
    }
    public void setIcon(int viewId, int resId) {
        ImageView iv = getView(viewId);
        if (iv != null) {
            iv.setImageResource(resId);
        }
    }
    public void setContentView(View contentView) {
        mContentView = contentView;
    }
    public View getContentView() {
        return mContentView;
    }
}


OK,弹窗有了,布局还没有的。在app下的layout中新建一个dialog_update_app_tip.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="@dimen/dp_270"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ImageView
        android:layout_width="@dimen/dp_270"
        android:layout_height="wrap_content"
        android:background="@mipmap/pic_update" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        android:paddingBottom="@dimen/dp_26">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dp_8"
            android:text="发现新版本"
            android:textColor="@color/black"
            android:textSize="@dimen/sp_16" />
        <TextView
            android:id="@+id/tv_update_info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/dp_12"
            android:paddingLeft="@dimen/dp_24"
            android:paddingRight="@dimen/dp_24"
            android:text="再也不会错过更多精彩啦!" />
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_50">
        <TextView
            android:id="@+id/tv_cancel"
            android:layout_width="@dimen/dp_0"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@drawable/shape_left_bg"
            android:foreground="@drawable/bg_white"
            android:gravity="center"
            android:text="下次再说"
            android:textColor="#597EF7"
            android:textSize="@dimen/sp_16" />
        <TextView
            android:id="@+id/tv_fast_update"
            android:layout_width="@dimen/dp_0"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:background="@drawable/shape_right_bg"
            android:foreground="@drawable/bg_white"
            android:gravity="center"
            android:text="立即更新"
            android:textColor="@color/white"
            android:textSize="@dimen/sp_16" />
    </LinearLayout>
</LinearLayout>


图片

20200831173916162.png

shape_left_bg.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:bottomLeftRadius="@dimen/dp_12"/>
    <solid android:color="@color/gray_white"/>
</shape>


shape_right_bg.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:bottomRightRadius="@dimen/dp_12"/>
    <solid android:color="@color/blue"/>
</shape>


布局也有了,现在要来显示这个弹窗了。


在MainActivity中

private AlertDialog updateAppDialog = null;//应用更新提示弹窗


  /**
     * 应用更新提示弹窗
     * @param downloadUrl 下载地址
     * @param updateLog  更新日志
     */
    private void showUpdateAppDialog(String downloadUrl,String updateLog) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context)
                .addDefaultAnimation()//默认弹窗动画
                .setCancelable(true)
                .setText(R.id.tv_update_info,updateLog)
                .setContentView(R.layout.dialog_update_app_tip)//载入布局文件
                .setWidthAndHeight(SizeUtils.dp2px(context, 270), ViewGroup.LayoutParams.WRAP_CONTENT)//设置弹窗宽高
                .setOnClickListener(R.id.tv_cancel, v -> {//取消
                    updateAppDialog.dismiss();
                }).setOnClickListener(R.id.tv_fast_update, v -> {//立即更新
                    //开始下载
                });
        updateAppDialog = builder.create();
        updateAppDialog.show();
    }


然后在检查的返回中调用


20200831174346418.png


再在灾害预警的返回中调用这个checkVersion()方法


2020090109162347.png


对了还有一个小问题,就是被和风给摆了一道,就是这个平台突然改了返回值,导致我拿数据时空对象了,然后程序崩溃了。然后我查了一下是搜索城市的返回数据发生了改变,也不知道为啥突然就改了一个参数的键,打开NewSearchCityResponse,将原来的statu替换成code,然后get和set方法也改一下。


20200831175524508.png


然后在使用的地方一一修改就可以了


20200831175701491.png


现在运行一下,不过你要改一下版本名,因为我平台上是2.2,所以我改成2.1,然后运行


20200831174840101.png


看起来还是不错的呀。然后继续往下走,


5.下载应用、安装应用


在MainActivity中新增如下方法:


  /**
     * 清除APK
     * @param apkName
     * @return
     */
    public static File clearApk(String apkName) {
        File apkFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), apkName);
        if (apkFile.exists()) {
            apkFile.delete();
        }
        return apkFile;
    }
    /**
     * 下载APK
     * @param downloadUrl
     */
    private void downloadApk(String downloadUrl) {
        clearApk("GoodWeather.apk");
        //下载管理器 获取系统下载服务
        DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(downloadUrl));
        //设置运行使用的网络类型,移动网络或者Wifi都可以
        request.setAllowedNetworkTypes(request.NETWORK_MOBILE | request.NETWORK_WIFI);
        //设置是否允许漫游
        request.setAllowedOverRoaming(true);
        //设置文件类型
        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
        String mimeString = mimeTypeMap.getMimeTypeFromExtension(MimeTypeMap.getFileExtensionFromUrl(downloadUrl));
        request.setMimeType(mimeString);
        //设置下载时或者下载完成时,通知栏是否显示
        request.setNotificationVisibility(request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
        request.setTitle("下载新版本");
        request.setVisibleInDownloadsUi(true);//下载UI
        //sdcard目录下的download文件夹
        request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "GoodWeather.apk");
        //将下载请求放入队列
        downloadManager.enqueue(request);
    }


这里我调用了系统的DownloadManager进行下载,在通知栏进行,不过这个需要配置一个广播接收器,新建一个DownloadApkReceiver

20200831175209511.png


package com.llw.goodweather.receiver;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import androidx.core.content.FileProvider;
import com.llw.goodweather.WeatherApplication;
import com.llw.goodweather.utils.Constant;
import com.llw.goodweather.utils.ToastUtils;
import java.io.File;
import java.util.Arrays;
/**
 * 下载APK广播
 */
public class DownloadApkReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) {//下载完成
            long downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1L);
            DownloadManager manager = (DownloadManager) WeatherApplication.getContext().getSystemService(Context.DOWNLOAD_SERVICE);
            DownloadManager.Query query = new DownloadManager.Query();
            query.setFilterById(downloadId);
            Cursor cursor = manager.query(query);
            if (!cursor.moveToFirst()) {
                cursor.close();
                return;
            }
            int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
            if (status == DownloadManager.STATUS_SUCCESSFUL) {//成功
                //安装
                installApk(context);
            }
        }
    }
  /**
     * 安装APK
     * @param context
     */
    public static void installApk(Context context) {
        File file = new File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                , "GoodWeather.apk");
        Intent intent = new Intent(Intent.ACTION_VIEW);
        // 由于没有在Activity环境下启动Activity,设置下面的标签
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        if(Build.VERSION.SDK_INT>=24) { //判读版本是否在7.0以上
            //参数1 上下文, 参数2 Provider主机地址 和配置文件中保持一致   参数3  共享的文件
            Uri apkUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
            //添加这一句表示对目标应用临时授权该Uri所代表的文件
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
        }else{
            intent.setDataAndType(Uri.fromFile(file),
                    "application/vnd.android.package-archive");
        }
        context.startActivity(intent);
    }
}


然后就需要在AndroidManifest.xml中进行配置了,

在application标签中增加


    <!--下载APK广播-->
        <receiver
            android:name=".receiver.DownloadApkReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
            </intent-filter>
        </receiver>
        <!--Android7.0以后读取文件需要配置Provider-->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.llw.goodweather.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <!--元数据-->
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>


这里还需要再添加一个权限,用于APP安装


<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /><!--应用安装-->

2021061712022581.png


注意一点


20200831180637809.png


然后在xml下新建一个file_paths.xml文件


20200831180659275.png


里面的包名还是用自己的


<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path path="Android/data/com.llw.goodweather.fileprovider/" name="files_root" />
    <external-path path="." name="external_storage_root" />
</paths>


然后在MainActivity中


20200901093414800.png


来演示一下


20200901093220571.gif


然后再加一个提示Toast


20200901093414800.png


OK,至此自动更新功能就写好了。

相关文章
|
1月前
|
人工智能 搜索推荐 物联网
Android系统版本演进与未来展望####
本文深入探讨了Android操作系统从诞生至今的发展历程,详细阐述了其关键版本迭代带来的创新特性、用户体验提升及对全球移动生态系统的影响。通过对Android历史版本的回顾与分析,本文旨在揭示其成功背后的驱动力,并展望未来Android可能的发展趋势与面临的挑战,为读者呈现一个既全面又具深度的技术视角。 ####
|
2月前
|
Java 程序员 开发工具
Android|修复阿里云播放器下载不回调的问题
虽然 GC 带来了很多便利,但在实际编码时,我们也需要注意对象的生命周期管理,该存活的存活,该释放的释放,避免因为 GC 导致的问题。
40 2
|
2月前
|
存储 API 数据库
uniapp APP自动更新组件
uniapp APP自动更新组件
87 1
|
2月前
|
开发工具 iOS开发 MacOS
【Mac_mistake】app不能安装在未命名需要OSv11.13或更高版本
【Mac_mistake】app不能安装在未命名需要OSv11.13或更高版本
112 0
|
4月前
|
开发工具 git 索引
repo sync 更新源码 android-12.0.0_r34, fatal: 不能重置索引文件至版本 ‘v2.27^0‘。
本文描述了在更新AOSP 12源码时遇到的repo同步错误,并提供了通过手动git pull更新repo工具来解决这一问题的方法。
162 1
|
4月前
|
开发工具 uml git
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
本文分享了下载AOSP源码的方法,包括如何使用repo工具和处理常见的repo sync错误,以及配置Python环境以确保顺利同步特定版本的AOSP代码。
580 0
AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
|
4月前
|
存储 监控 数据库
Android经典实战之OkDownload的文件分段下载及合成原理
本文介绍了 OkDownload,一个高效的 Android 下载引擎,支持多线程下载、断点续传等功能。文章详细描述了文件分段下载及合成原理,包括任务创建、断点续传、并行下载等步骤,并展示了如何通过多种机制保证下载的稳定性和完整性。
137 0
|
4月前
|
Ubuntu 开发工具 Android开发
Repo下载、编译AOSP源码:基于Ubuntu 21.04,android-12.1.0_r27
文章记录了作者在Ubuntu 21.04服务器上配置环境、下载并编译基于Android 12.1.0_r27版本的AOSP源码的过程,包括解决编译过程中遇到的问题和错误处理方法。
237 0
下一篇
DataWorks