Android开发android开发知识点安卓开发

Android APK更新和注意事项

2019-01-24  本文已影响11人  Sky_Blue
一、应用场景及介绍
  1. 不多讲,APP更新几乎每个APP都会用到。
  2. 更新APP的选择:
    一、根据自己APP使用的网格框架,自己写一套下载的逻辑,移植兼容性不强。
    二、用系统的下载器(DownloadManager),移植没兼容性问题。
二、更新的展示方式
  1. APP内UI直接展示。
  2. 退到后台,通知栏更新。
三、更新内容分析
  1. APK下载的URL,APK的版本号。
  2. 本地存储路径,APK本地存储名称等。
  3. 下载进度的回调,用于内部UI更新。
四、创建更新内容实体类
/**
 * 更新apk的Bean
 */

public class AppUpdateBean implements Serializable {
    // 更新APK的URL
    private String apkUrl;
    // 后台最新的版本号
    private int versionCode;

    // ==============附加信息可以不填写==============
    // apk文件名称(本地的名称)
    private String apkName;

    public AppUpdateBean(String apkUrl, String apkName, int versionCode) {
        this.apkUrl = apkUrl;
        this.apkName = apkName;
        this.versionCode = versionCode;
    }

    public String getApkUrl() {
        return apkUrl;
    }


    public int getVersionCode() {
        return versionCode;
    }

    public String getApkName() {
        return apkName;
    }
}   
五、定义下载进度回调监听接口
/**
 * 更新APK的监听
 */

public abstract class OnUpdateListener implements Serializable {
    /**
     * 下载失败
     */
    public abstract void onFailed(String msg);

    /**
     * 下载成功
     */
    public abstract void onSucceed(File apkFile);

    /**
     * 下载进度
     * @param total 总APK大小
     * @param current 当前进度
     * @param progress 进度百分比
     */
    public abstract void onProgress(int total, int current, float progress);
}
六、更新步骤分析
  1. 请求后台获取更新APK的内容。
  2. 判断后台APK的版本和当前使用APK的版本号,根据版本号情况进行更新。
  3. 下载前,判断本地有没有下载好,下载好根据版本号情况直接安装或者重装下载。
七、使用DownloadManager下载器进行下载更新
/**
 * 下载更新APP的工具类
 */

public class DownloadUtils {
    private static final String CONFIG = "APK_UPDATE";
    private static final String DOWNLOADED = "DOWNLOADED";
    // 默认APK本地名称
    public static final String DEF_APK_NAME = "update.apk";
    private final SharedPreferences mPreferences;
    // 下载器
    private DownloadManager downloadManager;
    private Context mContext;
    // 下载的ID
    private long downloadId = -1;
    // 下载要用到的类
    private AppUpdateBean mUpdateBean;
    private OnUpdateListener mOnUpdateListener;
    private static Handler mHandler = new Handler(Looper.getMainLooper());

    public DownloadUtils(Context context, AppUpdateBean updateBean) {
        if (mContext == null) {
            new RuntimeException("context is null");
        }
        this.mContext = context.getApplicationContext();
        this.mUpdateBean = updateBean;
        mPreferences = mContext.getSharedPreferences(CONFIG, Context.MODE_PRIVATE);
    }

    /**
     * 注意:要自己添加内存卡读取权限
     * 下载apk主要方法
     */
    public void downloadAPK() {
        // 1. 非空校验
        if (mContext == null || mUpdateBean == null) {
            return;
        }
        // 2. URL校验
        String url = mUpdateBean.getApkUrl();
        if (TextUtils.isEmpty(url) || !URLUtil.isNetworkUrl(mUpdateBean.getApkUrl())) {
            Toast.makeText(mContext, "APK下载地址不正确", Toast.LENGTH_SHORT).show();
            return;
        }
        // 1. 在这里要做一下校验
        File apkFile = getApkFile();
        // 判断有没有下载成功
        if (apkFile.exists() && apkFile.isFile() && apkFile.length() > 1024 && isDownload()) {
            try {
                // 判断下载好的APK版本和正在使用的APK版本
                int versionCode = AppUpdateUtils.getVersionCode(mContext);
                // 获取下载好的APK版本号
                PackageInfo packageInfo = mContext.getPackageManager()
                        .getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_ACTIVITIES);
                int apkVersionCode = packageInfo.versionCode;
                // 1. 下载好的APK是不是最新的
                // 2. 正在使用的APK是不是最新的
                if (apkVersionCode >= mUpdateBean.getVersionCode() && apkVersionCode > versionCode) {
                    if (mOnUpdateListener != null) {
                        mOnUpdateListener.onSucceed(apkFile);
                    } else {
                        installAPK();
                    }
                    return;
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        // 2. 删除APK
        deleteApkFile(apkFile);
        // 3. 创建下载任务
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
        // 移动网络情况下是否允许漫游
        request.setAllowedOverRoaming(false);
        request.allowScanningByMediaScanner();
        // 在通知栏中显示,默认就是显示的
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
        request.setTitle("APP名称");
        request.setDescription("版本更新");
        request.setVisibleInDownloadsUi(true);
        //4. 设置下载的路径
        request.setDestinationUri(Uri.fromFile(getApkFile()));
        // 获取DownloadManager
        if (downloadManager == null)
            downloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        // 将下载请求加入下载队列,加入下载队列后会给该任务返回一个long型的id,通过该id可以取消任务,重启任务、获取下载的文件等等
        if (downloadManager != null) {
            downloadId = downloadManager.enqueue(request);
        }
        // 注册广播接收者,监听下载完成状态
        mContext.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
        if (mOnUpdateListener != null) {
            // 开启个任务去每秒查询下载进度
            mHandler.postDelayed(mTask, 1000);
        }
    }

    private Runnable mTask = new Runnable() {
        @Override
        public void run() {
            if (mOnUpdateListener != null) {
                checkStatus();
                mHandler.postDelayed(mTask, 1000);
            }
        }
    };

    /**
     * 手动删除原来的APK,下载器不会覆盖。
     */
    private void deleteApkFile(File apkFile) {
        try {
            putDownload(false);
            if (apkFile.exists()) {
                apkFile.delete();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

    }


    /**
     * 广播监听下载完成
     */
    private BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 下载完成会调一次这里
            checkStatus();
        }
    };

    /**
     * 检查下载状态
     */
    private void checkStatus() {
        if (downloadId == -1) {
            return;
        }
        DownloadManager.Query query = new DownloadManager.Query();
        // 通过下载的id查找
        query.setFilterById(downloadId);
        Cursor cursor = downloadManager.query(query);
        if (cursor == null) {
            return;
        }
        if (cursor.moveToFirst()) {
            int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
            try {
                // 如果有下载监听就去查询文件下载进度
                if (mOnUpdateListener != null) {
                    //已经下载文件大小
                    int current = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                    //下载文件的总大小
                    int total = cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                    if (total >= 0 && current >= 0 && mOnUpdateListener != null) {
                        // 更新进度
                        float progress = current * 100f / total;
                        mOnUpdateListener.onProgress(total, current, progress);
                    }
                }
            } catch (Throwable e) {
            }
            switch (status) {
                // 下载暂停
                case DownloadManager.STATUS_PAUSED:
                    break;
                // 下载延迟
                case DownloadManager.STATUS_PENDING:
                    break;
                // 正在下载
                case DownloadManager.STATUS_RUNNING:
                    break;
                // 下载完成
                case DownloadManager.STATUS_SUCCESSFUL:
                    mHandler.removeCallbacks(mTask);
                    // 下载完成安装APK
                    putDownload(true);
                    // 有监听让用户去做
                    if (mOnUpdateListener != null) {
                        mOnUpdateListener.onSucceed(getApkFile());
                    } else {
                        installAPK();
                    }
                    cursor.close();
                    if (mContext != null) {
                        mContext.unregisterReceiver(receiver);
                    }
                    break;
                // 下载失败
                case DownloadManager.STATUS_FAILED:
                    mHandler.removeCallbacks(mTask);
                    if (mOnUpdateListener != null) {
                        mOnUpdateListener.onFailed("下载失败");
                    } else {
                        Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();
                    }
                    cursor.close();
                    break;
            }
        }
    }

    /**
     * 安装APK
     */
    private void installAPK() {
        // 安装APK
        AppFileProvider.installApk(mContext, getApkFile());
    }

    /**
     * 文件下载的路径
     */
    private File getApkFile() {
        String fileName = mUpdateBean.getApkName();
        if (TextUtils.isEmpty(fileName)) {
            fileName = DEF_APK_NAME;
        }
        return AppUpdateUtils.getApkFile(mContext, fileName);
    }


    /**
     * 缓存下载信息
     */
    private void putDownload(boolean isDownload) {
        if (mPreferences == null) {
            return;
        }
        mPreferences.edit().putBoolean(DOWNLOADED, isDownload).commit();
    }

    /**
     * 有没有下载完成
     */
    private boolean isDownload() {
        if (mPreferences == null) {
            return false;
        }
        return mPreferences.getBoolean(DOWNLOADED, false);
    }

    public void setOnUpdateListener(OnUpdateListener onUpdateListener) {
        mOnUpdateListener = onUpdateListener;
    }

    /**
     * 如果设置了下载进度回调,在Activity 的OnDestroy方法调用 一下。
     */
    public void stop() {
        try {
            mOnUpdateListener = null;
            mHandler.removeCallbacks(mTask);
            if (mContext != null) {
                mContext.unregisterReceiver(receiver);
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

下载APK封装类,主要下载方法是downloadAPK(),如果只是通知栏更新,就不用下载进度回调了。

八、下载用到的一些方法工具类封装。
/**
 * 兼容7.0文件路径配置
 */

public class AppFileProvider {

    /**
     * 安装APK
     */
    public static void installApk(Context context, File apk) {
        if (context == null || apk == null) {
            return;
        }
        // 1.修改文件权限
        setPermission(apk.getAbsolutePath());

        // 2. 安装APK
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        String type = "application/vnd.android.package-archive";
        if (Build.VERSION.SDK_INT >= 24) {
            Uri uriForFile = FileProvider.getUriForFile(
                    context,
                    context.getPackageName() + ".app.update.FileProvider",
                    apk
            );
            intent.setDataAndType(uriForFile, type);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        } else {
            intent.setDataAndType(Uri.fromFile(apk), type);
        }
        context.startActivity(intent);
    }

    /**
     * 修改文件权限
     */
    private static void setPermission(String absolutePath) {
        try {
            String command = "chmod " + "777" + " " + absolutePath;
            Runtime runtime = Runtime.getRuntime();
            runtime.exec(command);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
/**
 * APP更新工具类
 */

public class AppUpdateUtils {
    /**
     * 获取版本号
     */
    public static int getVersionCode(Context context) {
        if (getPackageInfo(context) != null) {
            try {
                return getPackageInfo(context).versionCode;
            } catch (Exception e) {
            }
        }
        return 0;
    }

    private static PackageInfo getPackageInfo(Context context) {
        if (context == null) {
            return null;
        }
        PackageInfo pi = null;

        try {
            PackageManager pm = context.getPackageManager();
            pi = pm.getPackageInfo(context.getPackageName(), 0);

            return pi;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return pi;
    }

    /**
     * 检查是否SDK准备好
     */
    private static boolean checkSDExist() {
        return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
    }

    /**
     * 创建apk下载文件
     * 这里如果有其它需求,可以改成你想要的下载路径
     */
    public static File getApkFile(Context context, String fileName) {
        // 创建目录
        File directory = null;
        if (checkSDExist()) {
            directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        }
        if (directory == null) {
            directory = context.getCacheDir();
        }
        if (!directory.exists()) {
            directory.mkdirs();
        }
        File apkFile = new File(directory, fileName);
        return apkFile;
    }
}
九、清单权限和7.0系统文件路径配置
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<!--安装权限-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
    android:allowBackup="true"
    android:supportsRtl="true">
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.app.update.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_app_update_paths" />
    </provider>
</application>
// ====文件:file_app_update_paths.xml====
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path
        name="root"
        path="" />
    <files-path
        name="files"
        path="." />

    <cache-path
        name="cache"
        path="." />

    <external-path
        name="external"
        path="." />

    <external-files-path
        name="external_file_path"
        path="." />
    <external-cache-path
        name="external_cache_path"
        path="." />

</paths>
十、测试
/**
 * 下载测试
 */
public class MainActivity extends AppCompatActivity {
    private TextView mTextView;
    private DownloadUtils mDownloadUtils;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = findViewById(R.id.tv);
    }

    public void update(View view) {
        // 申请权限
        XPermission.with(this)
                .permissions(Permission.STORAGE)
                .request(new PermissionListenerAdapter() {
                    @Override
                    public void onSucceed() {
                        downloadApk();
                    }
                });

    }

    /**
     * 下载代码
     */
    private void downloadApk() {
        String apkUrl = "https://xxx/app/B_2.8.3_0105_online.apk";
        AppUpdateBean bean = new AppUpdateBean(apkUrl, "CarHouse.apk", 123);
        mDownloadUtils = new DownloadUtils(MainActivity.this, bean);
        mDownloadUtils.setOnUpdateListener(new OnUpdateListener() {
            @Override
            public void onFailed(String msg) {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onSucceed(File apkFile) {
                // TODO 安装
                Toast.makeText(getApplicationContext(), "下载成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onProgress(int total, int current, float progress) {
                mTextView.setText(String.format("%.2f", progress) + "%");
            }
        });
        mDownloadUtils.downloadAPK();
    }

    @Override
    protected void onDestroy() {
        if (mDownloadUtils != null) {
            mDownloadUtils.stop();
        }
        super.onDestroy();

    }
}

测试说明:

  1. 测试没有请求后台接口的实现,实际开发是请求后台的接口,拿到APK更新数据。
  2. 如果只是通知栏更新,就直接创建DownloadUtils对象调一下downloadAPK()方法即可。
  3. 权限申请可以用自己项目的。
  4. 全部更新代码都在这里了,自己考过去就可以用了。
十一、注意事项
  1. Android 6.0权限申请。
  2. 7.0系统文件路径配置。
  3. 8.0系统清单权限配置。
上一篇下一篇

猜你喜欢

热点阅读