本篇文章主要实现在应用内发现新版本,用户点击下载apk,同时在通知栏下实现下载进度更新,下载完成后自动弹出安装窗口等等功能,来源于慕课网的视频整理,适合新手,做了详细的注释说明
不提供源码,但源码已全部奉上:
回调接口
/**
* Created by Administrator on 2017/10/1 0001.
* 回调接口:实现各种事件的监听回调
*/
public interface UpdateDownloadListener {
/**
* 下载开始回调
*/
public void onStarted();
/**
* 更新进度回调
* @param progress
* @param downloadUrl
*/
public void onProgressChanged(int progress , String downloadUrl);
/**
* 下载完成回调
* @param completeSize
* @param downloadUrl
*/
public void onFinished(int completeSize, String downloadUrl);
/**
* 下载失败回调
*/
public void onFailure();
}
Request
/**
* 真正负责我们文件的下载和线程间通信
*/
public class UpdateDownloadRequest implements Runnable {
//下载路径
private String downloadUrl;
//文件保存路径
private String localFilePath;
//事件回调
private UpdateDownloadListener downloadListener;
//下载的标志
private boolean isDownloading = false;
//文件长度
private long currentLength;
//Handler
private DownloadResponseHandler downloadHandler;
//构造方法对我们需要的参数进行初始化
public UpdateDownloadRequest(String downloadUrl, String localFilePath, UpdateDownloadListener downloadListener) {
this.downloadUrl = downloadUrl;
this.localFilePath = localFilePath;
this.downloadListener = downloadListener;
this.isDownloading = true;//构造方法完成以后表明已经开始下载
this.downloadHandler = new DownloadResponseHandler();
}
//真正的去建立连接的方法
private void makeRequest() throws InterruptedIOException, IOException {
if (Thread.currentThread().isInterrupted()) {//如果我们的当前线程没有被打断(即在正在后台运行)
try {
//创建URL对象
URL url = new URL(downloadUrl);
//通过url对象获取Connection对象:HttpURLConnection不需要导入第三方的框架就可以加载
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置一些属性
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setRequestProperty("Connection", "Keep-Alive");//保持连接
connection.connect();//阻塞我们的当前线程(所以会放在子线程中进行)
//连接建立以后获取文件的长度
currentLength = connection.getContentLength();
//开始下载
if (Thread.currentThread().isInterrupted()) {//健壮性判断
//真正完成文件的下载
downloadHandler.sendResponseMessage(connection.getErrorStream());
}
} catch (IOException e) {
//有异常:抛出
throw e;
}
}
}
//完成连接的建立
@Override
public void run() {
try {
makeRequest();
} catch (InterruptedIOException e) {
} catch (IOException e) {
}
}
/**
* 格式化数字:保证数字精确到小数点后两位
*/
private String getTwoPointFloatStr(float value) {
DecimalFormat fnum = new DecimalFormat("0.00");
return fnum.format(value);
}
/**
* 异常类型:包含下载过程中所有可能出现的异常情况
*/
public enum FailureCode {
UnKnowHost,
Socket,
ScoketTimeout,
ConnectTimeout,
IO,
HttpResponse,
JSON,
Interrupted
}
/**
* 用来真正的去下载文件,并发送消息和回调接口
* 完成我们消息的发送方法封装、消息处理方法的封装、文件下载方法封装
*/
public class DownloadResponseHandler {
//静态常量表明每种事件
protected static final int SUCCESS_MESSAGE = 0;
protected static final int FAILURE_MESSAGE = 1;
protected static final int START_MESSAGE = 2;
protected static final int FINISH_MESSAGE = 3;
protected static final int NETWORK_OFF = 4;
private static final int PROGRESS_CHANGE = 5;
private int mCompleteSize = 0;
private int progress = 0;
private Handler handler;//真正的完成线程间的通信
private DownloadResponseHandler() {
//创建依附于主线程的Handler:处理我们所有的事件
handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
handlerSelfMessage(msg);
}
};
}
/**
* 用来发送不同的消息类型对象对象
*/
protected void sendFinishMessage() {
sendMessage(obtainMessage(FINISH_MESSAGE, null));
}
private void sendProgressChangeMessage(int progress) {
sendMessage(obtainMessage(PROGRESS_CHANGE, new Object[]{progress}));
}
protected void sendFailureMessage(FailureCode failureCode) {
sendMessage(obtainMessage(FAILURE_MESSAGE, new Object[]{failureCode}));
}
protected void sendMessage(Message msg) {
if (null != null) {
//调用handler的方法
handler.sendMessage(msg);
} else {
//为空直接处理我们的message
handlerSelfMessage(msg);
}
}
/**
* 获取一个消息对象
*/
protected Message obtainMessage(int responseMessage, Object response) {
Message msg = null;
if (null != handler) {
//不为空直接调用handler的方法
msg = handler.obtainMessage(responseMessage, response);
} else {
//为空,我们就调取Message.obtain()获取Message,上面的最终也是调用这个方法获取Message
msg = Message.obtain();
msg.what = responseMessage;
msg.obj = response;
}
return msg;
}
//处理我们的各种消息:根据不同类型发送不同消息
protected void handlerSelfMessage(Message msg) {
Object[] response;
switch (msg.what) {
case FAILURE_MESSAGE:
response = (Object[]) msg.obj;
handlerFailureMessage(((FailureCode) response[0]));
break;
case PROGRESS_CHANGE:
response = (Object[]) msg.obj;
handlerProgressChangeMessage(((Integer) response[0]).intValue());
break;
case FINISH_MESSAGE:
onFinish();
break;
}
}
/**
* 各种消息的处理逻辑
*/
protected void handlerProgressChangeMessage(int progress) {
downloadListener.onProgressChanged(progress, "");
}
protected void handlerFailureMessage(FailureCode failureCode) {
onFailure(failureCode);
}
//外部接口的回调
public void onFinish() {
downloadListener.onFinished(mCompleteSize, "");
}
public void onFailure(FailureCode failureCode) {
downloadListener.onFailure();
}
//文件下载方法,会发送各种类型的事件:下载开始、下载进度更新、下载成功。。。
void sendResponseMessage(InputStream is) {
//建立文件读写流
RandomAccessFile randomAccessFile = null;
mCompleteSize = 0;//开始的完成进度0
//
try {
//定义自己一次读取多少到缓存,防止内存撑爆
byte[] buffer = new byte[1024];
//读写长度
int length = -1;
int limit = 0;
randomAccessFile = new RandomAccessFile(localFilePath, "rwd");//文件路径,可读可写模式
//循环从流中读取字节数组
while ((length = is.read(buffer)) != -1) {//不等于-1表明还没有读完
//当前是下载状态,把我们读到的字节写到本地
if (isDownloading) {
randomAccessFile.write(buffer, 0, length);//读写的长度
//累加长度
mCompleteSize += length;
//计算当前的下载进度
if (mCompleteSize < currentLength) {//完成的大小小于总大小,说明正在下载
progress = (int) Float.parseFloat(getTwoPointFloatStr(mCompleteSize / currentLength));
//发送进度:limit / 30 == 0表示隔30次更新一次进度,不用读一个字节就去更新下载进度
if (limit / 30 == 0 && progress <= 100) {//限制我们notification的更新频率
sendProgressChangeMessage(progress);
}
limit++;
}
}
}
//文件读取完成,发送完成通知
sendFinishMessage();
} catch (IOException e) {
//出现异常,发送失败信息
sendFailureMessage(FailureCode.IO);
} finally {
try {
//记得关闭流
if (null != is) {
is.close();
}
//关闭读写
if (null != randomAccessFile) {
randomAccessFile.close();
}
} catch (IOException e) {
//发送Io异常
sendFailureMessage(FailureCode.IO);
}
}
}
}
}
/**
* 下载调度管理器,调用我们的UpdateDownloadRequest,完成真正的下载,有两种实现方式:
* 1.直接起一个线程,线程中传入我们的UpdateDownloadRequest进行下载
* 2.初始化一个线程池,将我们的任务UpdateDownloadRequest添加到线程池当中,使用线程池实现UpdateManager
*/
public class UpdateManager {
//单例模式
private static UpdateManager manager;
//线程池
private ThreadPoolExecutor threadPoolExecutor;
//添加的任务
private UpdateDownloadRequest request;
private UpdateManager() {
threadPoolExecutor = (ThreadPoolExecutor) Executors.newCachedThreadPool();
}
//加载单例模式
static {
manager = new UpdateManager();
}
//获取单例模式
public static UpdateManager getInstance() {
return manager;
}
//开始下载
public void startDownloads(String downloadUrl, String localPath, UpdateDownloadListener listener) {
if (null != request) {
return;
}
//检查路径是否合法
checkLocalFilePath(localPath);
//request为null时,将request添加到我们的线程池当中
request = new UpdateDownloadRequest(downloadUrl, localPath, listener);
//开始真正的去下载任务
Future<?> future = threadPoolExecutor.submit(request);
}
//用来检查文件路径是否已经存在
private void checkLocalFilePath(String localPath) {
//获取问价夹的位置
File dir = new File(localPath.substring(0, localPath.lastIndexOf("/") + 1));
//检查文件夹是否存在
if(!dir.exists()){
//不存在创建文件夹
dir.mkdir();
}
File file = new File(localPath);
if(!file.exists()){
//文件不存在则创建
try {
file.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
/**
* 真正调用我们的UpdateManager完成下载功能
* 使用Serice:因为直接在我们的Activity启动线程,我们的Activity随时会被回收,所以就会产生僵尸
* 线程,所以用Service在后天默默下载
*/
public class UpdateService extends Service {
private String apkUrl;
private String filePath;
private NotificationManager notificationManager;
private Notification mNotification;
private PendingIntent contentIntent;
@Override
public void onCreate() {
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
//文件路径
filePath = Environment.getExternalStorageDirectory() + "/okamiy/QJFund.apk";
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (null == intent) {
//表明此次参数不对,下载失败,报异常
notifyUser(getString(R.string.update_download_failed), getString(R.string.update_download_failed_msg), 0);
//失败要停掉我们的Service
stopSelf();
}
//取出我们的Url
apkUrl = intent.getStringExtra("apkUrl");
//通知用户下载开始了
notifyUser(getString(R.string.update_download_start), getString(R.string.update_download_start_msg), 0);//刚开始进度为0
startDownload();
return super.onStartCommand(intent, flags, startId);
}
//下载:在回调中写你的逻辑
private void startDownload() {
UpdateManager.getInstance().startDownloads(apkUrl, filePath, new UpdateDownloadListener() {
@Override
public void onStarted() {
}
//进度改变通知用户
@Override
public void onProgressChanged(int progress, String downloadUrl) {
notifyUser(getString(R.string.update_download_processing), getString(R.string.update_download_processing), progress);
}
//下载完成通知用户
@Override
public void onFinished(int completeSize, String downloadUrl) {
notifyUser(getString(R.string.update_download_finish), getString(R.string.update_download_finish), 100);
//下载完成停止服务
stopSelf();
}
//下载失败通知用户
@Override
public void onFailure() {
notifyUser(getString(R.string.update_download_failed), getString(R.string.update_download_failed_msg), 0);//失败进度为0
//停止服务
stopSelf();
}
});
}
/**
* 更新我们的Notification来告知用户当前的下载进度
*
* @param result
* @param reason
* @param progress
*/
private void notifyUser(String result, String reason, int progress) {
//创建Notification:采用NotificationCompat兼容者构造模式创建,Android会帮我们做各个系统的Notification兼容,
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
//设置参数
builder.setSmallIcon(R.drawable.sample_footer_loading)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.sample_footer_loading_progress))
.setContentTitle(getString(R.string.app_name));
//判断是否下载
if (progress > 0 && progress < 100) {//正在下载,显示我们的progress
builder.setProgress(100, progress, false);//最大值,当前值,false表明我们需要具体的进度而不是转圈
} else {
//下载完成或者失败,隐藏progress
builder.setProgress(0, 0, false);
}
//可以被自动清除掉
builder.setAutoCancel(true);
//系统当前时间
builder.setWhen(System.currentTimeMillis());
builder.setTicker(result);
//进度完成就去安装,否则就是一个空的Intent
builder.setContentIntent(progress >= 100 ? getContentIntent()
: PendingIntent.getActivity(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT));
mNotification = builder.build();
//发送Notification
notificationManager.notify(0, mNotification);//id :0
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* 调用系统的安装程序进行安装
* @return
*/
public PendingIntent getContentIntent() {
//创建我们下载好的文件
File apkFile =new File(filePath);
//创建Intent
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://"+apkFile.getAbsolutePath()),
"application/vnd.android.package-archive");//data:文件路径 ,type:应用安装程序
//包装intent
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,intent,PendingIntent.FLAG_UPDATE_CURRENT);//参数4:标志位
return pendingIntent;
}
}
/**
* 自定义CommonDialog
*/
public class CommonDialog extends Dialog {
private TextView titleTV;
private TextView contentTV;
private Button sureBTN;
private Button cancelBTN;
private String title;//标题
private String content;//内容
private String leftBtnText;//左边按钮text
private String rightBtnText;//右边按钮text
private OnYesClickListener onYesClickListener;
private OnNoClickListener onNoClickListener;
public void setTitle(String title) {
this.title = title;
}
public void setContent(String content) {
this.content = content;
}
public void setLeftBtnText(String leftBtnText) {
this.leftBtnText = leftBtnText;
}
public void setRightBtnText(String rightBtnText) {
this.rightBtnText = rightBtnText;
}
public void setOnYesClickListener(OnYesClickListener onYesClickListener) {
this.onYesClickListener = onYesClickListener;
}
public void setOnNoClickListener(OnNoClickListener onNoClickListener) {
this.onNoClickListener = onNoClickListener;
}
public CommonDialog(Context context) {
super(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dialog_layout);
setCanceledOnTouchOutside(false);//点击空白不能取消dialog
initView();
initData();
initEvent();
}
/**
* 初始化事件
*/
private void initEvent() {
sureBTN.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(onYesClickListener!=null){
onYesClickListener.yesClick();
}
}
});
cancelBTN.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(onNoClickListener!=null){
onNoClickListener.noClick();
}
}
});
}
/**
* 初始化数据
*/
private void initData() {
titleTV.setText(title);
contentTV.setText(content);
sureBTN.setText(leftBtnText);
cancelBTN.setText(rightBtnText);
}
/**
* 初始化控件
*/
private void initView() {
titleTV= (TextView) findViewById(R.id.titleTV);
contentTV= (TextView) findViewById(R.id.contentTV);
sureBTN= (Button) findViewById(R.id.sureBTN);
cancelBTN= (Button) findViewById(R.id.cancelBTN);
}
/**
* 设置确定按钮和取消按钮的接口回调
*/
public interface OnYesClickListener {
void yesClick();
}
public interface OnNoClickListener{
void noClick();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/titleTV"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
android:text="Title"
android:textSize="18sp" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#000000" />
<TextView
android:id="@+id/contentTV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="80dp"
android:padding="10dp"
android:text="content"
android:textSize="16sp" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#000000" />
<LinearLayout
android:layout_marginTop="2dp"
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal">
<Button
android:id="@+id/sureBTN"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_weight="1"
android:text="立即更新"
android:textSize="16sp" />
<Button
android:id="@+id/cancelBTN"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_weight="1"
android:text="下次再说"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<Button
android:id="@+id/login"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="检测更新"
/>
</RelativeLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.login);
//1.点击按钮更新
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkVersion();
}
});
//2.app启动的时候检查更新
checkVersion();
}
/**
* 检查更新:发送http请求到服务器,服务器返回最新的版本号,如果当期应用的版本号小于返回的版本号,则进行应用
* 更新,反之则不需要更新
*/
private void checkVersion() {
final CommonDialog dialog = new CommonDialog(this);
dialog.setTitle("ApkUpdate");
dialog.setContent("发现新版本,请及时更新");
dialog.setLeftBtnText("立即更新");
dialog.setRightBtnText("稍后再说");
dialog.setOnYesClickListener(new CommonDialog.OnYesClickListener() {
@Override
public void yesClick() {
//启动Service
Intent intent = new Intent(MainActivity.this, UpdateService.class);
//传入版本号:取服务器apk地址
intent.putExtra("apkUrl", "换成你的apk地址即可");
//启动Service
startService(intent);
}
});
dialog.setOnNoClickListener(new CommonDialog.OnNoClickListener() {
@Override
public void noClick() {
dialog.dismiss();
}
});
dialog.show();
}
}