安卓多线程下载文件(三)

安卓多线程下载文件(三)

接着 安卓多线程下载文件(二) 的内容,继续介绍下载页面相关的内容。

一、创建下载界面的布局文件

就简单设计一个布局

一个TextView 显示下载的字节数和总长度

一个ProgressBar 显示进度

三个按钮 开始下载,暂停下载,取消下载
在这里插入图片描述

布局很简单 ,没什么需要多说的,直接上代码

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="true"
    android:fitsSystemWindows="true"
    android:orientation="vertical">


    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/color_status_bar_back_ground"
        android:orientation="horizontal">

        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/down_load_detail_title_image_view_back"
            android:layout_width="@dimen/menu_back_width"
            android:layout_height="@dimen/menu_back_height"
            android:layout_gravity="center"
            android:background="@drawable/selector_default"
            android:clickable="true"
            android:focusable="true"
            android:paddingStart="@dimen/menu_back_padding_left"
            android:paddingLeft="@dimen/menu_back_padding_left"
            android:paddingTop="@dimen/menu_back_padding_top"
            android:paddingEnd="@dimen/menu_back_padding_right"
            android:paddingRight="@dimen/menu_back_padding_right"
            android:paddingBottom="@dimen/menu_back_padding_bottom"
            android:src="@drawable/ic_back_ffffff" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/down_load_detail_title"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@string/str_download"
            android:textColor="@color/color_white" />


        <View
            android:id="@+id/down_load_detail_view_help"
            android:layout_width="@dimen/menu_back_width"
            android:layout_height="@dimen/menu_back_height" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp1"
        android:background="@color/color_divider" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingTop="@dimen/dp10"
            android:paddingBottom="@dimen/dp10">

            <View
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp5" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/down_load_detail_text_view_app_name"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingStart="@dimen/dp15"
                android:paddingLeft="@dimen/dp15"
                android:paddingEnd="@dimen/dp15"
                android:paddingRight="@dimen/dp15"
                android:text="" />

            <View
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp10" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/down_load_detail_text_view_download_state"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:paddingStart="@dimen/dp15"
                android:paddingLeft="@dimen/dp15"
                android:paddingEnd="@dimen/dp15"
                android:paddingRight="@dimen/dp15"
                android:text="@string/str_downloading" />

            <androidx.appcompat.widget.AppCompatTextView
                android:id="@+id/down_load_detail_text_view_download_percent"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingStart="@dimen/dp15"
                android:paddingLeft="@dimen/dp15"
                android:paddingEnd="@dimen/dp15"
                android:paddingRight="@dimen/dp15"
                android:text="" />

            <View
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp2" />

            <ProgressBar
                android:id="@+id/down_load_detail_progress_bar"
                style="?android:attr/progressBarStyleHorizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:indeterminate="false"
                android:max="100"
                android:paddingStart="@dimen/dp15"
                android:paddingLeft="@dimen/dp15"
                android:paddingEnd="@dimen/dp15"
                android:paddingRight="@dimen/dp15"
                android:progress="0"
                android:secondaryProgress="0" />


            <LinearLayout
                android:id="@+id/down_load_detail_ll_start"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/selector_default"
                android:clickable="true"
                android:focusable="true">

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="@dimen/menu_back_width"
                    android:layout_height="@dimen/menu_back_height"
                    android:layout_gravity="center"
                    android:paddingLeft="@dimen/dp14"
                    android:paddingTop="@dimen/dp10"
                    android:paddingRight="@dimen/dp14"
                    android:paddingBottom="@dimen/dp10"
                    app:srcCompat="@drawable/shape_point_gray" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/down_load_detail_start"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:paddingStart="@dimen/dp0"
                    android:paddingLeft="@dimen/dp0"
                    android:paddingTop="@dimen/dp8"
                    android:paddingEnd="@dimen/dp10"
                    android:paddingRight="@dimen/dp10"
                    android:paddingBottom="@dimen/dp8"
                    android:text="@string/str_start_download" />
            </LinearLayout>


            <View
                android:id="@+id/down_load_detail_line_after_start"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp1"
                android:background="@color/color_divider" />

            <LinearLayout
                android:id="@+id/down_load_detail_ll_pause"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/selector_default"
                android:clickable="true"
                android:focusable="true">

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="@dimen/menu_back_width"
                    android:layout_height="@dimen/menu_back_height"
                    android:layout_gravity="center"
                    android:paddingLeft="@dimen/dp14"
                    android:paddingTop="@dimen/dp10"
                    android:paddingRight="@dimen/dp14"
                    android:paddingBottom="@dimen/dp10"
                    app:srcCompat="@drawable/shape_point_gray" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/down_load_detail_pause"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:paddingStart="@dimen/dp0"
                    android:paddingLeft="@dimen/dp0"
                    android:paddingTop="@dimen/dp8"
                    android:paddingEnd="@dimen/dp15"
                    android:paddingRight="@dimen/dp15"
                    android:paddingBottom="@dimen/dp8"
                    android:text="@string/str_pause_download" />
            </LinearLayout>


            <View
                android:id="@+id/down_load_detail_line_after_pause"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp1"
                android:background="@color/color_divider" />

            <LinearLayout
                android:id="@+id/down_load_detail_ll_cancel"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/selector_default"
                android:clickable="true"
                android:focusable="true">

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="@dimen/menu_back_width"
                    android:layout_height="@dimen/menu_back_height"
                    android:layout_gravity="center"
                    android:paddingLeft="@dimen/dp14"
                    android:paddingTop="@dimen/dp10"
                    android:paddingRight="@dimen/dp14"
                    android:paddingBottom="@dimen/dp10"
                    app:srcCompat="@drawable/shape_point_gray" />

                <androidx.appcompat.widget.AppCompatTextView
                    android:id="@+id/down_load_detail_cancel"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:paddingStart="@dimen/dp0"
                    android:paddingLeft="@dimen/dp0"
                    android:paddingTop="@dimen/dp8"
                    android:paddingEnd="@dimen/dp15"
                    android:paddingRight="@dimen/dp15"
                    android:paddingBottom="@dimen/dp8"
                    android:text="@string/str_cancel_download" />
            </LinearLayout>

            <View
                android:id="@+id/down_load_detail_line_after_cancel"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp1"
                android:background="@color/color_divider" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>


二、添加权限

因为需要从服务器上把文件下载到手机的本地存储空间,所以需要 网络权限,读写存储空间的权限;

在项目的 \app\src\main\AndroidManifest.xml 路径中,在AndroidManifest.xml 文件中添加 必要的权限:

<uses-permission android:name="android.permission.INTERNET" /> <!-- 连接网络权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 存储空间 读取权限,查看文件图片等需要 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 存储空间 写入权限,下载的文件保存(写入)到本地存储空间 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- 请求安装未知来源的应用 所需的权限 -->

三、设计NewVersionDownLoadDetailActivity

下载详情页,用来实时显示下载进度的,并且下载的控制按钮页通过这个页面来完成。

下载前需要先检查下有没有WRITE_EXTERNAL_STORAGE 权限,没有的话,需要动态申请,判断有没有某个权限的代码很简单,这里就不贴了;

其它一些辅助的类的代码也都是很基础的,都不贴出来了;

package com.linkpoon.mixed.activity;

import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.provider.Settings;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ProgressBar;

import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;

import com.linkpoon.mixed.R;
import com.linkpoon.mixed.action.CheckNewVersionTask;
import com.linkpoon.mixed.action.TaskListener;
import com.linkpoon.mixed.base.BaseActivity;
import com.linkpoon.mixed.bean.ServerVersion;
import com.linkpoon.mixed.commom.Global;
import com.linkpoon.mixed.constants.DownloadConstants;
import com.linkpoon.mixed.service.NewVersionDownLoadService;
import com.linkpoon.mixed.update.VersionParser;
import com.linkpoon.mixed.util.DialogTool;
import com.linkpoon.mixed.util.CommonUtil;
import com.linkpoon.mixed.util.LanguageUtil;
import com.linkpoon.mixed.util.LogUtil;
import com.linkpoon.mixed.util.PermissionCheck2;
import com.linkpoon.mixed.util.TipUtil;
import com.linkpoon.mixed.util.ToastUtil;

import java.io.File;
import java.io.InputStream;
import java.util.Locale;


public class NewVersionDownLoadDetailActivity extends BaseActivity implements View.OnClickListener {

    private static final String TAG = Global.TAG_PREFIX + "NewVersion";

    public static final String INTENT_KEY_PATH = "apkSavePath";
    public static final String INTENT_KEY_VERSION = "serverVersion";

    private NewVersionDownLoadService downLoadService;

    private final ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            downLoadService = ((NewVersionDownLoadService.DownloadBinder) iBinder).getNewVersionDownLoadService();
            checkPermissionBeforeDownload();//onServiceConnected
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };
    private String fileSavePath;
    private ServerVersion serverVersion;

    private boolean isServiceBound = false;
    private AppCompatImageView imageViewBack;
    private LinearLayoutCompat viewStart;
    private LinearLayoutCompat viewPause;
    private LinearLayoutCompat viewCancel;
    private View lineAfterStart;
    private View lineAfterPause;
    private View lineAfterCancel;

    private AppCompatTextView textViewAppName;
    private AppCompatTextView textViewDownloadState;
    private AppCompatTextView textViewDownloadPercent;
    private ProgressBar progressBar;

    private boolean isDownloading = false;
    private boolean isInstalling = false;

    private boolean isCanceled = false;
    private boolean isPaused = false;

    private Handler msgHandler;

    private final TipUtil tipUtil = new TipUtil();

    private void tipNoPermission(String string) {
        tipUtil.showTipDialog(NewVersionDownLoadDetailActivity.this, string);
    }

    private void closeMyTipDialog() {
        if (tipUtil != null) {
            tipUtil.closeTipDialog();
        }
    }

    private final ActivityResultCallback<Boolean> resultCallbackStorage = new ActivityResultCallback<Boolean>() {
        @Override
        public void onActivityResult(Boolean result) {

            if (!result) {
                // 用户 拒绝授权 存储 权限
                boolean b = ActivityCompat.shouldShowRequestPermissionRationale(NewVersionDownLoadDetailActivity.this, Manifest.permission.CAMERA);
                if (!b) { //用户 拒绝了 权限 而且勾选了 不再询问
                    tipNoPermission(NewVersionDownLoadDetailActivity.this.getString(R.string.str_prompt_need_storage_permission));
                }
            } else {
                startDownLoad();// 用户 授权了 存储 权限
            }

        }
    };

    private final ActivityResultContracts.RequestPermission requestPermissionStorage = new ActivityResultContracts.RequestPermission();
    private final ActivityResultLauncher<String> launcherStorage = registerForActivityResult(requestPermissionStorage, resultCallbackStorage);

    private InstallResultReceiver installResultReceiver;


    private final ActivityResultCallback<ActivityResult> resultCallbackForUnknownApp = new ActivityResultCallback<ActivityResult>() {

        @Override
        public void onActivityResult(ActivityResult result) {
            if (result.getResultCode() == RESULT_OK) {
                installApk();//请求授权安装未知来源应用权限 回调
            }
        }
    };
    private final ActivityResultContracts.StartActivityForResult startActivityForResultForUnknownApp = new ActivityResultContracts.StartActivityForResult();
    private final ActivityResultLauncher<Intent> launcherForUnknownApp = registerForActivityResult(startActivityForResultForUnknownApp, resultCallbackForUnknownApp);


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LogUtil.i(TAG, "onCreate");
        setContentView(R.layout.activity_version_down_load_detail);
        imageViewBack = findViewById(R.id.down_load_detail_title_image_view_back);

        textViewAppName = findViewById(R.id.down_load_detail_text_view_app_name);
        textViewDownloadState = findViewById(R.id.down_load_detail_text_view_download_state);
        textViewDownloadPercent = findViewById(R.id.down_load_detail_text_view_download_percent);
        progressBar = findViewById(R.id.down_load_detail_progress_bar);

        viewStart = findViewById(R.id.down_load_detail_ll_start);
        viewPause = findViewById(R.id.down_load_detail_ll_pause);
        viewCancel = findViewById(R.id.down_load_detail_ll_cancel);

        lineAfterStart = findViewById(R.id.down_load_detail_line_after_start);
        lineAfterPause = findViewById(R.id.down_load_detail_line_after_pause);
        lineAfterCancel = findViewById(R.id.down_load_detail_line_after_cancel);

        imageViewBack.setOnClickListener(this);
        viewStart.setOnClickListener(this);
        viewPause.setOnClickListener(this);
        viewCancel.setOnClickListener(this);

        msgHandler = new Handler(msgCallback);
        fileSavePath = getIntent().getStringExtra(INTENT_KEY_PATH);
        serverVersion = (ServerVersion) getIntent().getSerializableExtra(INTENT_KEY_VERSION);
        if (serverVersion != null) {
            textViewAppName.setText(serverVersion.getVersionName() + " " + serverVersion.getVersionCode());
        }
        checkDownloadPram();
        startServiceByCheck();
        myBindService();
        registerInstallReceiver();
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        LogUtil.i(TAG, "onDestroy");
        closeMyTipDialog();//onDestroy
        myUnBindService();//onDestroy
        stopServiceByCheck();//onDestroy
        unRegisterInstallReceiver();
    }

    private void registerInstallReceiver() {
        if (installResultReceiver == null) {
            installResultReceiver = new InstallResultReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_PACKAGE_ADDED);
            filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
            filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
            filter.addAction(Intent.ACTION_MY_PACKAGE_REPLACED);
            filter.addDataScheme("package");//涉及到存储卡状态改变的,需要加上这个,不然接收不到广播
            this.registerReceiver(installResultReceiver, filter);
            LogUtil.i(TAG, "注册 包 添加、改变、替换的 广播接收器");
        }
    }

    private void unRegisterInstallReceiver() {
        if (installResultReceiver != null) {
            this.unregisterReceiver(installResultReceiver);
            installResultReceiver = null;
        }
    }

    private void startServiceByCheck() {
        Intent intent = new Intent(this, NewVersionDownLoadService.class);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            // 安卓8.0之前的版本适配
            LogUtil.i(TAG, "8.0之前 startService");
            startService(intent);
        } else {
            //安卓8.0及之后版本适配
            LogUtil.i(TAG, "8.0及之后 startForegroundService ");
            startForegroundService(intent);
        }
    }

    private void stopServiceByCheck() {
        if (downLoadService != null) {
            LogUtil.i(TAG, "downLoadService.stopForeground(true)");
            downLoadService.stopForeground(true);
            LogUtil.i(TAG, "downLoadService.stopSelf()");
            downLoadService.stopSelf();
        }
        Intent intentStop = new Intent(this, NewVersionDownLoadService.class);
        LogUtil.i(TAG, "stopService(intentStop)");
        stopService(intentStop);
    }

    private void myBindService() {
        LogUtil.i(TAG, "bindService 绑定");
        Intent intent = new Intent(this, NewVersionDownLoadService.class);
        isServiceBound = bindService(intent, serviceConnection, BIND_AUTO_CREATE);
    }

    private void myUnBindService() {
        if (isServiceBound) {
            LogUtil.i(TAG, "unbindService 解绑");
            unbindService(serviceConnection);
            isServiceBound = false;
        }
    }

    /**
     * 该Activity 启动模式 为singleTask
     * 点击下载的那个通知 会启动该Activity
     * 会调用该方法
     * 达到 点击通知进来自动开始下载
     */
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        checkPermissionBeforeDownload();//onNewIntent
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            myBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    private void myBack() {
        if (downLoadService != null && downLoadService.isDownloading()) {
            String downloading = getString(R.string.str_downloading);
            ToastUtil.showText(this, downloading);
            moveTaskToBack(true);
        } else {
            finish();
        }
    }

    @Override
    public void onClick(View view) {

        int id = view.getId();
        if (id == R.id.down_load_detail_title_image_view_back) {
            myBack();
        } else if (id == R.id.down_load_detail_ll_start) {
            checkPermissionBeforeDownload();// 点击 开始下载 按钮
        } else if (id == R.id.down_load_detail_ll_pause) {
            pauseClicked();// 点击 暂停下载 按钮
        } else if (id == R.id.down_load_detail_ll_cancel) {
            cancelClicked();// 点击 取消下载 按钮
        }

    }

    private void checkPermissionBeforeDownload() {
        PermissionCheck2 permissionCheck2 = new PermissionCheck2();
        if (!permissionCheck2.hasPerPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            // 没有 存储 权限 去申请
            try {
                launcherStorage.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
            } catch (ActivityNotFoundException e) {
                LogUtil.e(TAG, "Not found Activity can handle request permission!", e);
            }
        } else {
            // 已经获得 存储 权限
            startDownLoad();  // 已经获得 存储 权限
        }
    }


    private void startDownLoad() {
        if (downLoadService != null) {
            checkDownloadPram();
            if (serverVersion == null) {
                String getVersionInfoError = getString(R.string.str_get_version_info_exception_from_the_server);
                //Toast.makeText(this, getVersionInfoError, Toast.LENGTH_SHORT).show();
                showToast(getVersionInfoError);
                return;
            }
            // 开始下载
            // 这里直接默认使用 4条线程 来下载同一个文件,实际上可以根据业务需求灵活传参
            downLoadService.startDownLoad(fileSavePath, serverVersion, 4, msgHandler);
        }
    }


    private void checkDownloadPram() {
        if (fileSavePath == null) {
            fileSavePath = CommonUtil.getAppFilesDir(this);
        }
        if (serverVersion == null) {
            boolean isEn = LanguageUtil.currentLanguageIsEnglish(this);
            TaskListener<String> taskListener = new TaskListener<String>() {
                @Override
                public void onPreExecute() {

                }

                @Override
                public void onProgressUpdate(Integer arg) {

                }

                @Override
                public void onCancelled() {

                }

                @Override
                public void onSuccess(String data) {
                    InputStream is = CommonUtil.getStringStream(data);
                    if (is != null) {
                        try {
                            serverVersion = VersionParser.getServerVersionInfo(is);// 将流解析成版本
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }

                @Override
                public void onFailure(String message) {

                }
            };
            CheckNewVersionTask checkNewVersionTask = new CheckNewVersionTask(isEn, this, taskListener);
            checkNewVersionTask.execute();
        }
    }


    private void pauseClicked() {
        if (downLoadService != null) {
            downLoadService.pauseDownLoad();
            isPaused = true;
        }
    }

    private void cancelClicked() {
        if (downLoadService != null) {
            downLoadService.cancelDownLoad();
            isCanceled = true;
        }
    }


    private final Handler.Callback msgCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message message) {
            switch (message.what) {
                case DownloadConstants.MSG_NORMAL_INIT:

                    break;
                case DownloadConstants.MSG_NORMAL_START:

                    break;
                case DownloadConstants.MSG_NORMAL_UPDATE_PROGRESS:
                    Bundle bundle = message.getData();
                    if (bundle != null) {
                        long downedByte = (long) bundle.get(DownloadConstants.BUNDLE_KEY_DOWNED_BYTE);
                        // LogUtil.i(TAG, "downedByte = " + downedByte + " byte");
                        long totalByte = (long) bundle.get(DownloadConstants.BUNDLE_KEY_TOTAL_BYTE);
                        // LogUtil.i(TAG, "totalByte = " + totalByte + " byte");
                        long progress = (long) bundle.get(DownloadConstants.BUNDLE_KEY_PROGRESS);
                        onProgress(downedByte, totalByte, (int) progress);
                        if (downLoadService != null) {
                            downLoadService.onProgress(downedByte, totalByte, (int) progress);
                        }
                    }
                    break;
                case DownloadConstants.MSG_NORMAL_COMPLETE:
                    onSuccess();
                    if (downLoadService != null) {
                        downLoadService.onSuccess();
                    }
                    break;
                case DownloadConstants.MSG_NORMAL_PAUSE:
                    onPaused();
                    if (downLoadService != null) {
                        downLoadService.onPaused();
                    }
                    break;
                case DownloadConstants.MSG_NORMAL_CANCELED:
                    onCanceled();
                    if (downLoadService != null) {
                        downLoadService.onCanceled();
                    }
                    break;
                case DownloadConstants.MSG_NORMAL_FAILED:
                    onFailed();
                    if (downLoadService != null) {
                        downLoadService.onFailed();
                    }
                    break;
                default:
                    break;
            }
            return true;
        }
    };


    public void onProgress(long downedByte, long totalByte, int progress) {
        if (progressBar != null) {
            progressBar.setProgress(progress);
        }
        if (textViewDownloadPercent != null) {
            // 把 byte 换算成 MB 并保留两位小数
            String down = String.format(Locale.getDefault(), "%.2f", (downedByte * 1.0 / 1024 / 1024));
            String total = String.format(Locale.getDefault(), "%.2f", (totalByte * 1.0 / 1024 / 1024));
            String downedSize = down + " MB";
            String totalSize = total + " MB";
            String info = downedSize + "/" + totalSize + "  " + progress + "%";
            textViewDownloadPercent.setText(info);
        }
        if (textViewDownloadState != null) {
            textViewDownloadState.setText(R.string.str_downloading);
        }
    }


    public void onSuccess() {
        // 下载新版apk文件成功 回调
        if (textViewDownloadState != null) {
            textViewDownloadState.setText(R.string.str_down_load_apk_success);
        }
        if (progressBar != null) {
            progressBar.setProgress(100);
        }
        if (textViewDownloadPercent != null) {
            textViewDownloadPercent.setText("100%");
        }

        if (viewStart != null) {
            viewStart.setVisibility(View.GONE);
        }
        if (lineAfterStart != null) {
            lineAfterStart.setVisibility(View.GONE);
        }
        if (viewPause != null) {
            viewPause.setVisibility(View.GONE);
        }
        if (lineAfterPause != null) {
            lineAfterPause.setVisibility(View.GONE);
        }
        if (viewCancel != null) {
            viewCancel.setVisibility(View.GONE);
        }
        if (lineAfterCancel != null) {
            lineAfterCancel.setVisibility(View.GONE);
        }

        installApk();// 下载完成 的回调

    }

    public void onFailed() {
        if (textViewDownloadState != null) {
            textViewDownloadState.setText(R.string.str_down_load_apk_failed);
        }
    }


    public void onPaused() {
        if (textViewDownloadState != null) {
            textViewDownloadState.setText(R.string.str_pause_download);
        }
    }


    public void onCanceled() {
        if (textViewDownloadState != null) {
            textViewDownloadState.setText(R.string.str_cancel_download);
        }
    }

    public void deleteApkFile() {
        if (serverVersion != null) {
            File apkFile = new File(fileSavePath, serverVersion.getVersionName());
            boolean b = apkFile.delete();
            LogUtil.i(TAG, "APK文件删除结果:" + b);
        }
    }

    private Intent intentInstall;

    /**
     * 安装apk
     */
    private void installApk() {
        if (fileSavePath == null) {
            return;
        }
        if (serverVersion == null) {
            return;
        }
        File apkFile = new File(fileSavePath, serverVersion.getVersionName());

        if (!apkFile.exists() || apkFile.length() <= 0 || !apkFile.isFile()) {
            return;
        }

        try {
            //apk的路径 /storage/emulated/0/Android/data/包名/files/xxx.apk
            LogUtil.i(TAG, "apk的路径 " + apkFile.getAbsolutePath());
            intentInstall = new Intent();
            intentInstall.setAction(Intent.ACTION_VIEW);
            Uri contentUri;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                LogUtil.i(TAG, "> = android 7.0 ,intentInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)");
                contentUri = FileProvider.getUriForFile(this, this.getPackageName() + ".FileProvider", apkFile);
                LogUtil.i(TAG, "> = android 7.0 ,contentUri.getPath() " + contentUri.getPath());
            } else {
                contentUri = Uri.fromFile(apkFile);
                LogUtil.i(TAG, "< android 7.0 ,contentUri.getPath() " + contentUri.getPath());
            }
            intentInstall.setDataAndType(contentUri, "application/vnd.android.package-archive");
            LogUtil.i(TAG, "intentInstall.setDataAndType");
            //intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            //LogUtil.i(TAG, "intentInstall.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)");
            // 注意 不能加 Intent.FLAG_ACTIVITY_NEW_TASK 这个flag ,
            // 实测 有这样的bug :
            // 安装完成后 会显示两个按钮 一个完成,一个打开
            // 如果用户选择点击 打开,那么会出现 再次下载apk完成后,再调用该方法 会出现不能安装,也不报错的奇怪问题

            int targetSdkV = this.getApplicationInfo().targetSdkVersion;
            LogUtil.i(TAG, "当前的 targetSdkVersion 是 " + targetSdkV);
            if (targetSdkV <= 22) {
                LogUtil.i(TAG, "targetSdkVersion<=22,默认获得所有权限,直接去安装");
                startInstallActivity(intentInstall);
                // 通过Intent安装APK文件
                return;
            }

            //兼容8.0
            if (Build.VERSION.SDK_INT >= 26) {
                boolean hasInstallPermission = this.getPackageManager().canRequestPackageInstalls();
                // 注意 targetSdkVersion 必须 大于等于 26 , canRequestPackageInstalls 才能返回正确的结果
                // 否则即使 已经获得 权限,但还是返回 false
                LogUtil.i(TAG, "> = android 8.0 ,canRequestPackageInstalls " + hasInstallPermission);
                if (!hasInstallPermission) {
                    //请求安装未知应用来源的权限
                    LogUtil.i(TAG, "请求安装未知应用来源的权限");
                    //ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, REQUEST_CODE);
                    // 因为这个权限不是运行时权限,不能使用 ActivityCompat.requestPermissions 动态申请权限的 方式 来申请
                    // 只能引导用户手动去系统设置中 授权未知来源应用安装权限
                    showAskForPermissionDialog();
                    return;
                }
            }

            startInstallActivity(intentInstall);
        } catch (Exception e) {
            LogUtil.e(TAG, "安装应用出错 ", e);
            e.printStackTrace();
        }

    }


    private void startInstallActivity(Intent intent) {
        try {
            if (!getPackageManager().queryIntentActivities(intent, 0).isEmpty()) {
                LogUtil.i(TAG, "启动 安装应用 的系统activity ");
                startActivity(intent);
            } else {
                LogUtil.e(TAG, "未查询到 安装应用 的Activities");
            }
        } catch (Exception e) {
            LogUtil.e(TAG, "使用系统标准接口安装apk出错 " ,e);
        }
    }


    private void openSystemSettingForUnknownAppRes() {
        try {
            LogUtil.i(TAG, "请求根据包名打开对应的设置界面 请求授权安装未知来源应用权限");
            Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
            if (Build.VERSION.SDK_INT >= 26) {
                intent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
                LogUtil.i(TAG, "安卓8.0 intent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)");
            }
            intent.setData(Uri.parse("package:" + getPackageName())); // 根据包名打开对应的设置界面
            launcherForUnknownApp.launch(intent);
        } catch (Exception e) {
            LogUtil.e(TAG, "打开设置界面出错");
            e.printStackTrace();
        }
    }


    private void showAskForPermissionDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle(R.string.str_confirm_authorization_please);
        builder.setMessage(R.string.str_authorization_explain);
        // 确定按钮
        DialogInterface.OnClickListener positiveButtonClickListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                openSystemSettingForUnknownAppRes();
            }
        };
        builder.setPositiveButton(R.string.str_ok, positiveButtonClickListener);
        // 取消按钮
        DialogInterface.OnClickListener negativeButtonClickListener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        };

        builder.setNegativeButton(R.string.str_cancel, negativeButtonClickListener);
        AlertDialog noticeDialog = builder.create();
        noticeDialog.setCanceledOnTouchOutside(false);
        DialogTool.setAlertDialogWindow(noticeDialog);
        if (!isFinishing()) {
            noticeDialog.show();
            //DialogTool.setAlertDialogButtonColor(noticeDialog);
            DialogTool.setAlertDialogFocus(noticeDialog);
        }
    }


    public class InstallResultReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {

            String action = intent.getAction();
            LogUtil.i(TAG, "InstallResultReceiver.onReceive:" + action);
            if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                LogUtil.i(TAG, "新增了一个应用:" + intent.getPackage());
            } else if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
                LogUtil.i(TAG, "本应用更新(新版本覆盖了一个旧版本):" + intent.getPackage());
                deleteApkFile();
            } else if (Intent.ACTION_PACKAGE_REPLACED.equals(action)) {
                LogUtil.i(TAG, "一个应用被替换:" + intent.getPackage());
                deleteApkFile();
            }

        }
    }

}


四、开启下载

通过 安卓多线程下载文件(一) 的 HttpEngine 工具类 访问服务器上apk文件的描述信息,
得到一个流,再把流转换成ServerVersion 对象,再打开NewVersionDownLoadDetailActivity 下载即可;

这里模拟下载的过程
所以 只写一些伪代码


	public void test() {    
	 Runnable runnable=new Runnable() {
	            @Override
	            public void run() {
	                queryApkInfo();
	            }
	        };
	 Thread t=new Thread(runnable);
	  t.start();
	}
	
	public void queryApkInfo() {    
	    String[] serverUrls ={"http://www.xxx.cn//Version-CN.xml"}// 这里需要 换成 真实的 文件描述地址
	 	String response = HttpEngine.getInstance().sendRequests(serverUrls, METHOD_GET, null, null, 0);
	     if (response == null || response.isEmpty()) {
            return ;
        }
        getNewVersionInfoSuccess(response);
	}

	 private void getNewVersionInfoSuccess(String data) {
	        ServerVersion  serverVersion = null;
	        InputStream is = CommonUtil.getStringStream(data);
	        if (is != null) {
	            try {
	                serverVersion = VersionParser.getServerVersionInfo(is);// 将流解析成版本
	            } catch (Exception e) {
	                isChecking = false;
	                e.printStackTrace();
	            }
	
	            if (serverVersion == null || serverVersion.getVersionApkUrl() == null) {
	                return;
	            }
	            turnToDownLoadDetailActivity(serverVersion );
	        }
	    }


    /**
     * 跳转到下载详情页并开始下载
     */
    public void turnToDownLoadDetailActivity(final ServerVersion serverVersion) {
        if (mSavePath == null) {
            mSavePath = CommonUtil.getAppFilesDir(mActivity);
        }
        Intent intent = new Intent(mActivity, NewVersionDownLoadDetailActivity.class);
        intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_PATH, mSavePath);
        intent.putExtra(NewVersionDownLoadDetailActivity.INTENT_KEY_VERSION, serverVersion);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        mActivity.startActivity(intent);
    }
    

五、小结

OK,多线程下载文件的关键之处都介绍得差不多了,剩下的细节还有挺多的,每个都详细介绍需要太多时间,就不一一介绍了。

六、参考文献

java多线程下载文件(断点下载、进度展示、网速展示)

【Java多线程】如何使用Java多线程下载网络文件 断点续传

java 多线程下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值