安卓多线程下载文件(三)
接着 安卓多线程下载文件(二) 的内容,继续介绍下载页面相关的内容。
一、创建下载界面的布局文件
就简单设计一个布局
一个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,多线程下载文件的关键之处都介绍得差不多了,剩下的细节还有挺多的,每个都详细介绍需要太多时间,就不一一介绍了。