最近要重构下载部分的代码,想想这正好是一个典型的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实现。