MVP+DownloadManager实现android下载功能

本文介绍了一个典型的MVP模式实现的下载功能案例。详细解释了Model、View和Presenter三个组件如何协作完成下载任务,包括下载流程、状态更新及UI交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近要重构下载部分的代码,想想这正好是一个典型的MVP模式实现的功能,看过网上很多MVP的例子,都是以登录为例,没有看到下载相关的,干脆按自己的理解写一个算了。

M负责下载功能的具体实现;
V就是activity;
P负责M和V之间的交互;

M和V都有自己的接口类,activity直接调用的是present,先上个工程截图:
这里写图片描述

一幅图描述它们之间的关系:
这里写图片描述

先分析下activity,当有新版本时,展示版本变更,并提供两个按钮供用户选择:立即更新或者不更新,下载完成后还要点击安装,此外当下载失败、下载完成还要在页面上做相应的提示,所以activity应具有下面的接口:

public interface IDownloadView {
    //开始下载
    public void downloadStart();
    //正在下载
    public void downloadRunning(int progress);
    //下载成功
    public void downloadSuccess();
    //下载失败
    public void downloadFailed(int reason);
    //下载暂停
    public void downloadPaused(int reason);
    //不进行更新
    public void noDownload();
    //安装apk失败
    public void installApkFailed(int reason);
    //强制更新,隐藏取消更新
    public void isForceUpdate();
}

M负责下载功能的具体实现,相应用户的点击事件,接口如下:

public interface IDownloadModel {
    //开始下载
    public void startDownload(DownloadPresenter.IDownloadStatusCB cb);
    //安装apk
    public int installDownloadApk();
    //检查是否需要强制升级
    public boolean checkIsForceUpdate();
}

然后是最重要的presenter,它负责M和V的交互,持有它们的接口,activity是通过presenter实现了对M层的调用:

public class DownloadPresenter {

    private final String CLASS_TAG = getClass().getSimpleName();
    private IDownloadView mIDownloadView;
    private IDownloadModel mIDownloadModel;
    private IDownloadStatusCB iDownloadStatusCB = new IDownloadStatusCB() {
        @Override
        public void getStatus(int status, int reason, int progress) {
            Log.i_ui(CLASS_TAG, "get status:" + status + " reason:" + reason + " progress:" + progress);
            switch (status){

                case DownloadManager.STATUS_PENDING:
                    mIDownloadView.downloadStart();
                    break;

                case DownloadManager.STATUS_PAUSED:
                    mIDownloadView.downloadPaused(reason);
                    break;

                case DownloadManager.STATUS_FAILED:
                    mIDownloadView.downloadFailed(reason);
                    break;

                case DownloadManager.STATUS_SUCCESSFUL:
                    mIDownloadView.downloadSuccess();
                    break;

                case DownloadManager.STATUS_RUNNING:
                    mIDownloadView.downloadRunning(progress);
                    break;
            }
        }
    };

    public DownloadPresenter(Context context, IDownloadView downloadView, UpdateInfo updateInfo){
        //构造函数中初始化两个接口
        mIDownloadView = downloadView;
        //updateInfo是升级信息类,传入DownloadModel
        mIDownloadModel = new DownloadModel(context, updateInfo);
    }

    //P中的下载方法,调用M层,传入回调接口
    public void startDownload(){
        mIDownloadModel.startDownload(iDownloadStatusCB);
    }

    //暂时不升级,通知activity
    public void noDownload(){
        mIDownloadView.noDownload();
    }

    //安装apk,通知activity
    public void installDownloadApk(){
        int install = mIDownloadModel.installDownloadApk();
        if (install == Constants.APK_IS_DOWNLOADING){
            mIDownloadView.installApkFailed(install);
        }
    }
    //M层和presenter之间的回调接口,M通过这个接口把下载状态通知到presenter
    public interface IDownloadStatusCB {
        void getStatus(int status, int reason, int progress);
    }

    public void checkIsForceUpdate(){
        //是强制升级,通知到activity
        if (mIDownloadModel.checkIsForceUpdate()){
            mIDownloadView.isForceUpdate();
        }
    }
}

M层,使用DownloadManager来实现下载功能,并通过一个计时器和timerTask查询下载进度,将下载状态通过回调通知presenter:

public class DownloadModel implements IDownloadModel {

    private Context mContext;
    private UpdateInfo mUpdateInfo;
    private static final String TAG = DownloadModel.class.getSimpleName();
    private DownloadManager mDownloadManager;
    private static final String APP_NAME = "xxxxx";
    private DownloadPresenter.IDownloadStatusCB iDownloadStatusCB;
    private String apkFilePath;
    private String apkFileName;

    private TimerTask mTimerTask;
    private Timer mTimer;
    private long downloadId;
    /**
     * 网络原因导致处于pause状态,计算pause次数
     */
    private int pauseTimes;
    /**
     * 最大重试次数
     */
    private static final int MAX_RETRY_TIMES = 20;


    public DownloadModel(Context context, UpdateInfo mUpdateInfo) {
        this.mContext = context;
        this.mUpdateInfo = mUpdateInfo;
    }

    @Override
    public void startDownload(DownloadPresenter.IDownloadStatusCB cb) {
        //接收来自presenter的回调
        iDownloadStatusCB = cb;
        if (!Utils.checkNetworkAvailable(mContext)){
            //无网络通知presenter
            iDownloadStatusCB.getStatus(DownloadManager.STATUS_FAILED, Constants.MESSAGE_TYPE_UPGRADE_NETWORKNOTAVAILABLE, 0);
            return;
        }

        apkFilePath = mContext.getExternalFilesDir(null).getPath();
        apkFileName = APP_NAME + "_" + mUpdateInfo.getServerVersion() + ".apk";
        Log.i(TAG, "apkFilePath:" + apkFilePath);
        Log.i(TAG, "apkFileName:" + apkFileName);

        mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
        String mimeType = "application/vnd.android.package-archive";
        String address = mUpdateInfo == null ? null : mUpdateInfo.getUrl();
        Log.i(TAG,"download apk address:" + address);
        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(address));
        request.setDestinationInExternalFilesDir(mContext,null, APP_NAME + "_" + mUpdateInfo.getServerVersion() + ".apk");
        //需要添加权限<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
        request.setMimeType(mimeType);
        request.allowScanningByMediaScanner();
        request.setVisibleInDownloadsUi(false);

        iDownloadStatusCB.getStatus(DownloadManager.STATUS_PENDING, 0, 0);
        downloadId = mDownloadManager.enqueue(request);

        Log.i(getClass().getSimpleName(), "goDownloadApk_download_id:" + downloadId);
        //将downloadID存入SharedPreferences
        CacheManager.setCurrentDownloadId(mContext, downloadId);

        refreshProcess();
    }

    @Override
    public int installDownloadApk() {
        Log.i(TAG, "toInstallPage...");

        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        DownloadManager.Query query = new DownloadManager.Query();
        Cursor cursor = null;
        try {
            cursor = mDownloadManager.query(query);
            if (cursor != null && cursor.moveToFirst())
            {
                long id = getCurrentDownloadId(mContext.getApplicationContext());
                Log.i("VersionPageA", "toInstallPage_download_id:" + downloadId);
                Log.i("VersionPageA", "toInstallPage_download_id:" + id);
                int fileUriIdx = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI);
                String fileUri = cursor.getString(fileUriIdx);
                String filePath ="";
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
                    if (fileUri != null) {
                        filePath = Uri.parse(fileUri).getPath();
                    }
                } else {
                    //Android 7.0以上的方式:请求获取写入权限,这一步报错
                    //过时的方式:DownloadManager.COLUMN_LOCAL_FILENAME
                    int fileNameIdx = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
                    filePath = cursor.getString(fileNameIdx);
                }

                Log.i(TAG, "filePath:" + filePath);
                if(filePath!=null && !filePath.equals("")){
                    String filename = filePath.substring(filePath.lastIndexOf('/') + 1, filePath.length());
                    Uri newFileUri = Uri.withAppendedPath(Uri.fromFile(mContext.getExternalFilesDir(null)), filename);
                    Log.i(TAG, "newFileUri:" + newFileUri);
                    intent.setDataAndType(newFileUri, mDownloadManager.getMimeTypeForDownloadedFile(id));
                    mContext.startActivity(intent);
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            if(cursor != null){
                cursor.close();
            }
        }

        return Constants.APK_INSTALL_DONE;
    }

    @Override
    public boolean checkIsForceUpdate() {
        return  mUpdateInfo.isForceUpdate;
    }

    /**
     * 查询当前下载进度
     */
    private void queryStautsAndProcess() {
        Log.i(TAG, "queryStautsAndProcess called...");
        //从SharedPreferences中取出downloadID
        downloadId = CacheManager.getCurrentDownloadId(mContext.getApplicationContext());
        DownloadManager.Query mQuery = new DownloadManager.Query().setFilterById(downloadId);
        Cursor cursor = null;

        try {
            cursor = mDownloadManager.query(mQuery);
            if (cursor != null && cursor.moveToFirst()) {
                //查询下载状态
                int downloadStatus = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
                //查询下载原因,失败时有用    
                int downloadReason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON));
                Log.i(TAG, "downloadId:" + downloadId + "-----" + "downloadStatus:" + downloadStatus);
                //查询已下载部分
                long bytes_downloaded = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                //查询总大小
                long bytes_total = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                int downProgress = (int) ((bytes_downloaded * 100) / bytes_total);

                //处理下载对应状态,并回调到presenter
                if (downloadStatus == DownloadManager.STATUS_PAUSED) {
                    iDownloadStatusCB.getStatus(DownloadManager.STATUS_PAUSED, downloadReason, downProgress);
                    pauseTimes++;
                    if (pauseTimes == MAX_RETRY_TIMES){
                        //取消计时器
                        mTimer.cancel();
                        iDownloadStatusCB.getStatus(DownloadManager.STATUS_FAILED, Constants.APK_DOWNLOAD_TRY_MAX_TIMES, downProgress);
                    }
                } else if (downloadStatus == DownloadManager.STATUS_FAILED) {
                    mTimer.cancel();
                    iDownloadStatusCB.getStatus(DownloadManager.STATUS_FAILED, downloadReason, downProgress);
                } else if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
                    mTimer.cancel();
                    iDownloadStatusCB.getStatus(DownloadManager.STATUS_SUCCESSFUL, downloadReason, 100);

                } else {
                    Log.i(TAG, "process--->" + downProgress);
                    iDownloadStatusCB.getStatus(DownloadManager.STATUS_RUNNING, downloadReason, downProgress);
                }

            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }


    /**
     * 定时1s刷新下载进度,初始化计时器和任务
     */
    private void refreshProcess() {
        Log.i(TAG, "refreshProcess called...");
        long downloadId = getCurrentDownloadId(mContext);
        Log.i(TAG, "downloadId:" + downloadId);
        if (downloadId <= 0) {
            return;
        }

        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                queryStautsAndProcess();
            }
        };

        if (mTimer == null) mTimer = new Timer();
        mTimer.schedule(mTimerTask, 0, 200);
    }
}

presenter通过startDownload方法,调用IDownloadModel.startDownload(),并将callback传入,在DownloadModel中通过callback将下载状态、进度回调到presenter:
这里写图片描述
downloadModel把下载状态通知到present,presenter再通过IDownloadView通知到activity,activity是实现了IDownloadView :

public class UpdateDialogActivity extends ECMActivity implements IDownloadView, View.OnClickListener{

    private final String CLASS_TAG = getClass().getSimpleName();

    private ImageView newVerionIV;
    private LinearLayout llDownload;
    private TextView txtBgDownload;
    private CheckBox cbChoose;
    private RelativeLayout rlType;
    private TextView txtContent;
    private Button btnCancel;
    private Button btnDownload;
    public NumberProgressBar npbProgress;
    private TextView txtDownloading;
    private ScrollView scrollView;
    private int detailInfoTVHeight;
    private boolean isForceUpdate;
    private DownloadPresenter mDownloadPresenter;
    private NotificationManager notificationManager;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(getClass().getSimpleName(), "onCreate...");
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_update_dialog);

        UpdateInfo updateInfo = getUpdateInfo();

        initWindowSize();
        initViews(updateInfo);
        adjustViews();

        mDownloadPresenter = new DownloadPresenter(getApplicationContext(), this, updateInfo);
        //进入activity就查询是否是强制升级,如果是,隐藏不升级按钮或者将不升级
        //按钮的点击响应设置为退出app
        mDownloadPresenter.checkIsForceUpdate();

    }

    private void initViews(UpdateInfo updateInfo){
        Log.i(getClass().getSimpleName(), "initViews...");
        newVerionIV = (ImageView) findViewById(R.id.newVerionIV);
        llDownload = (LinearLayout) findViewById(R.id.ll_download);
        txtBgDownload = (TextView) findViewById(R.id.txt_bg_download);
        cbChoose = (CheckBox) findViewById(R.id.cb_choose);
        rlType = (RelativeLayout) findViewById(R.id.rl_type);
        txtContent = (TextView) findViewById(R.id.txt_content);
        btnCancel =(Button) findViewById(R.id.btn_cancel);
        btnDownload = (Button) findViewById(R.id.btn_download);
        npbProgress = (NumberProgressBar) findViewById(R.id.npb_progress);
        txtDownloading = (TextView) findViewById(R.id.txt_downloading);
        scrollView = (ScrollView) findViewById(R.id.scrollView);

        btnCancel.setOnClickListener(this);
        btnDownload.setOnClickListener(this);
        npbProgress.setOnClickListener(this);
        txtBgDownload.setOnClickListener(this);

        txtContent.setText(updateInfo.getUpgradeDetailInfo());
        btnCancel.setVisibility(updateInfo.isForceUpdate ? View.GONE : View.VISIBLE);
        txtBgDownload.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG);

        //根据内容,动态改变升级变更的textview高度
        txtContent.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
                if (txtContent.getMeasuredHeight() > detailInfoTVHeight){
                    changeUpdateInfoLayoutHeight();
                }
            }
        });

    }

    private void initWindowSize(){
        Log.i(getClass().getSimpleName(), "initWindowSize...");
        WindowManager m = getWindowManager();
        Display d = m.getDefaultDisplay();  //为获取屏幕宽、高
        WindowManager.LayoutParams p = getWindow().getAttributes();  //获取对话框当前的参数值
        p.height = (int) (d.getHeight() * 0.5);   //高度设置为屏幕的1.0
        p.width = (int) (d.getWidth() * 0.75);    //宽度设置为屏幕的0.8
//        p.alpha = 1.0f;      //设置本身透明度
//        p.dimAmount = 0.0f;      //设置黑暗度
        getWindow().setAttributes(p);
    }

    private void adjustViews(){
        Log.i(CLASS_TAG, "adjustViews called...");

        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) newVerionIV.getLayoutParams();
        GlobalData globalData = (GlobalData) getApplication();
        layoutParams.height = 350 * globalData.mScreenHeight / 1920;
        newVerionIV.setLayoutParams(layoutParams);
    }

    private void changeUpdateInfoLayoutHeight(){
        Log.i(getClass().getSimpleName(), "changeUpdateInfoLayoutHeight called...");

        GlobalData globalData = (GlobalData) getApplication();
        detailInfoTVHeight = txtContent.getMeasuredHeight();
        Log.i(getClass().getSimpleName(), "detailInfoTVHeight:" + detailInfoTVHeight);
        if (detailInfoTVHeight > globalData.mScreenHeight / 3){
            LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) scrollView.getLayoutParams();
            layoutParams.height = globalData.mScreenHeight / 3;
            scrollView.setLayoutParams(layoutParams);
        }
    }

    private UpdateInfo getUpdateInfo(){
        GlobalData globalData = (GlobalData) getApplication();
        return globalData.getmUpdateInfo();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            /**下次再说**/
            case R.id.btn_cancel:
                Log.i(getClass().getSimpleName(), "btn_cancel clicked...");
                //点击取消也通过presenter处理
                mDownloadPresenter.noDownload();
                break;
            /**立即更新**/
            case R.id.btn_download:
                notificationManager = (NotificationManager) getApplicationContext()
                        .getSystemService(Context.NOTIFICATION_SERVICE);
                Log.i(getClass().getSimpleName(), "btn_download clicked...");
                mDownloadPresenter.startDownload();
                break;
            /**点击安装**/
            case R.id.npb_progress:
                mDownloadPresenter.installDownloadApk();
                break;
            /**后台下载**/
            case R.id.txt_bg_download:
                break;
            default:
                break;
        }
    }

    @Override
    public void downloadStart() {
        Log.i(CLASS_TAG, "downloadStart");
        //开始下载刷新UI要在主线程运行
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                createDownloadNotice(Constants.NOTICE_ID_UPGRADE_APK_IS_DOWNLOADING, 0);
                llDownload.setVisibility(View.GONE);
                txtDownloading.setVisibility(View.VISIBLE);
                txtDownloading.setText(getString(R.string.downloading_wait));
            }
        });
    }

    @Override
    public void downloadRunning(final int progress) {
        Log.i(CLASS_TAG, "downloadRunning,progress:" + progress);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                createDownloadNotice(Constants.NOTICE_ID_UPGRADE_APK_IS_DOWNLOADING, progress);
                npbProgress.setVisibility(View.VISIBLE);
                llDownload.setVisibility(View.GONE);
                txtDownloading.setVisibility(View.VISIBLE);
                npbProgress.setProgress(progress);
            }
        });
    }

    @Override
    public void downloadSuccess() {
        Log.i(CLASS_TAG, "downloadSuccess");
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                createDownloadNotice(Constants.NOTICE_ID_UPGRADE_APK_DOWNLOAD_SUCC, 100);
                txtDownloading.setVisibility(View.GONE);
                npbProgress.setProgress(100);
                npbProgress.resetTextColor();
            }
        });
    }

    @Override
    public void downloadFailed(final int reason) {
        Log.i(CLASS_TAG, "downloadFailed, reason:" + reason);
        runOnUiThread(new Runnable() {
            @Override
            public void run() {

                createDownloadNotice(Constants.NOTICE_ID_UPGRADE_APK_DOWNLOAD_FAILED, Constants.CARD_STATUS_NO_REASON);
                switch (reason){
                    case Constants.MESSAGE_TYPE_UPGRADE_NETWORKNOTAVAILABLE:
                        txtDownloading.setVisibility(View.VISIBLE);
                        txtDownloading.setText(getString(R.string.RegistrationProgressActivity_unable_to_connect));
                        break;

                    case Constants.APK_DOWNLOAD_TRY_MAX_TIMES:
                        txtDownloading.setVisibility(View.VISIBLE);
                        txtDownloading.setText(getString(R.string.RegistrationProgressActivity_unable_to_connect));
                        break;

                    default:
                        txtDownloading.setVisibility(View.VISIBLE);
                        txtDownloading.setText(getString(R.string.RegistrationProgressActivity_unable_to_connect));
                        break;
                }
            }
        });
    }

    @Override
    public void downloadPaused(int reason) {
        Log.i(CLASS_TAG, "downloadPaused,reason:" + reason);
    }

    @Override
    public void noDownload() {
        Log.i(CLASS_TAG, "noDownload...");
        finish();
    }

    @Override
    public void installApkFailed(int reason) {
        if (reason == Constants.APK_IS_DOWNLOADING){
            Toast.makeText(getApplicationContext(),
                    getResources().getString(R.string.downloading_wait), Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    public void isForceUpdate() {
        Log.i(CLASS_TAG, "force update...");
        isForceUpdate = true;
        rlType.setVisibility(View.GONE);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {

        return super.onKeyDown(keyCode, event);
    }

    private void createDownloadNotice(int noticeID, int reason){

        //此处省略
}

上面的代码有省略,但已经可以看出MVP的整个框架了。按我的理解,MVP适用于一些activity面向用户的接口不多,但功能实现较复杂的场景,登录页面就两个edittext输入用户名、密码,一个登录按钮,然后是与后台的数据交互;类似的,绑定注册也可以通过MVP实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值