本篇文章主要实现在应用内发现新版本,用户点击下载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(); } }