preface
Some time ago wrote an APP automatic update download article automatic update, a method to fix, using the system DownloadManager method super simple to achieve the download of APK, but there are a lot of netizens reflect that there are some models above this method can not be downloaded, after the small series of experiments in some models will indeed have this problem, So the download part will have to be done in another way. I happened to see a lot of articles on the Internet about using Retrofit to download and monitor progress, and I’ve been reading Retrofit stuff but haven’t had a chance to use it, so I just wanted to practice with it. In the end, I used Retrofit + OkHttp + RxBus + Notification + Service to implement the function of automatically updating and downloading APK. You can not only solve the problem of App update, but also learn some good technology through practice. Github address: github.com/shanyao0/Do… Star and Fork, thank you…
The principle of
The basic principle and method of use with automatic update, a method to fix the same we do not know can refer to here, the same method to fix super simple, but only this time the force grid is higher, better compatibility of models. This manual implementation of the download and system notification progress function.
- Retrofit2 and OKHTTP implement apK downloads
- Custom classes Implement Retrofit2’s Callback class in which files are written to the IO stream and download progress is subscribed to using RxBus
- The custom class implements okHttp3’s ResponseBody class and uses RxBus in it to publish download progress information
- Use Retrofit in a Service to download files in the background
- Send Notifaction to the front screen of the notification bar to show progress
So I hope you can follow my steps to implement this feature step by step, so that you can not only use the high standard technology in your project, but also have a preliminary understanding of these technologies.
Implementation steps
This class mainly writes two management update and popbox methods, relatively simple, with automatic update, a method to do almost except for the download part
Public void checkUpdate(final Boolean isToast) {/** * Get the updated content and the latest version */ String version_info = "Updated content \n" +" 1. \n" + "2. Unified format \n" +" "; int mVersion_code = DeviceUtils.getVersionCode(mContext); int nVersion_code = 2; if (mVersion_code < nVersion_code) { showNoticeDialog(version_info); } else {if (isToast) {toast.maketext (mContext, "already updated ", toast.length_short).show(); }} /** * Displays the update dialog ** @param version_info */ private void showNoticeDialog(String version_info) {alertdialog.builder builder = new AlertDialog.Builder(mContext); Builder.settitle (" update prompt "); builder.setMessage(version_info); Builder. SetPositiveButton (" update now ", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); mContext.startService(new Intent(mContext, DownLoadService.class)); }}); Builder. SetNegativeButton (" update ", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); }}); Dialog noticeDialog = builder.create(); noticeDialog.show(); }Copy the code
2. Initialize RxBus for simple encapsulation
The use of RxBus can be seen here implementing the event Bus with RxJava
import rx.Observable; import rx.subjects.PublishSubject; import rx.subjects.SerializedSubject; import rx.subjects.Subject; public class RxBus { private static volatile RxBus mInstance; private final Subject bus; private RxBus() { bus = new SerializedSubject<>(PublishSubject.create()); } /** * @return */ public static RxBus getDefault() {RxBus RxBus = mInstance; if (mInstance == null) { synchronized (RxBus.class) { rxBus = mInstance; if (mInstance == null) { rxBus = new RxBus(); mInstance = rxBus; } } } return rxBus; Public void post(Object o) {bus.onnext (o); Public Observable toObservable(Class eventType) {return Observable toObservable(Class eventType) {return Observable toObservable(Class eventType) {return bus.ofType(eventType); }}Copy the code
3. Customize ResponseBody FileResponseBody
public class FileResponseBody extends ResponseBody { Response originalResponse; public FileResponseBody(Response originalResponse) { this.originalResponse = originalResponse; } @Override public MediaType contentType() { return originalResponse.body().contentType(); } @Override public long contentLength() { return originalResponse.body().contentLength(); } @Override public BufferedSource source() { return Okio.buffer(new ForwardingSource(originalResponse.body().source()) { long bytesReaded = 0; @Override public long read(Buffer sink, long byteCount) throws IOException { long bytesRead = super.read(sink, byteCount); bytesReaded += bytesRead == -1 ? 0 : bytesRead; RxBus.getDefault().post(new FileLoadingBean(contentLength(), bytesReaded)); return bytesRead; }}); }}Copy the code
Please ask me why I wrote it like this. I got it from the official demo of OPHTTP3. If you have the energy to study it, please don’t forget to tell me then
4. Customize the Callback class FileCallback
An abstract class implements the onResponse method of Callback and saves the file locally using the IO stream. Two abstract methods are defined for subclasses to implement. OnSuccess () calls the file back to the implementation class to install APK when the read/write is done. OnLoading () calls progress and total back to the implementation class by subscribing to download progress during file reading and writing to display progress information in real time in notifications
Public Abstract class FileCallback implements Callback{/** * subscriptions */ private CompositeSubscription rxSubscriptions = new CompositeSubscription(); /** * destFileDir */ private String destFileDir; /** * The name of the destination file */ private String destFileName; public FileCallback(String destFileDir, String destFileName) { this.destFileDir = destFileDir; this.destFileName = destFileName; subscribeLoadProgress(); } public void onSuccess(File File); Public abstract void onLoading(long progress, long total); */ @override public void onResponse(Call Call, Response Response) {try {saveFile(Response); } catch (Exception e) { e.printStackTrace(); }} /** * Writes files through THE IO stream */ public File saveFile(Response Response) throws Exception {InputStream in = null; FileOutputStream out = null; byte[] buf = new byte[2048]; int len; try { File dir = new File(destFileDir); if (! dir.exists()) { dir.mkdirs(); } in = response.body().byteStream(); File file = new File(dir,destFileName); out = new FileOutputStream(file); while ((len = in.read(buf)) ! = -1){ out.write(buf,0,len); } onSuccess(file); unSubscribe(); return file; }finally { in.close(); out.close(); }} /** * Private void subscribeLoadProgress() {rxsubscriptions.add (rxbus.getdefault ()) .toObservable(FileLoadingBean.class) .onBackpressureBuffer() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1() { @Override public void call(FileLoadingBean fileLoadEvent) { onLoading(fileLoadEvent.getProgress(), fileLoadEvent.getTotal()); }})); */ private void unSubscribe() {if (! rxSubscriptions.isUnsubscribed()) { rxSubscriptions.unsubscribe(); }}}Copy the code
The entity classes for Progress and Total are saved
Public class FileLoadingBean {/** * file size */ long total; /** * download size */ long progress; public long getProgress() { return progress; } public long getTotal() { return total; } public FileLoadingBean(long total, long progress) { this.total = total; this.progress = progress; }}Copy the code
5. Download and install APK using Retrofit2 and OKHTTP in the background Service, and send notifications to the foreground to show the download progress
For more information about Retrofit and OKHTTP, please refer to Retrofit2 by Hongyang Daishen
Public class DownLoadService extends Service {/** * Specifies the path for storing destination files. */ private String destFileDir = Environment.getExternalStorageDirectory().getAbsolutePath() + File .separator + "M_DEFAULT_DIR"; Private String destFileName = "shan_yao.apk"; /** * destFileName = "shan_yao.apk"; private Context mContext; private int preProgress = 0; private int NOTIFY_ID = 1000; private NotificationCompat.Builder builder; private NotificationManager notificationManager; private Retrofit.Builder retrofit; /** * why is the downloaded logic called in this method? Instead of onCreate? */ @override public int onStartCommand(Intent Intent, int flags, int startId) {mContext = this; loadFile(); return super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { return null; } /** * download file */ private void loadFile() {initNotification(); if (retrofit == null) { retrofit = new Retrofit.Builder(); } retrofit. BaseUrl (" http://112.124.9.133:8080/parking-app-admin-1.0/android/manager/adminVersion/ ") .client(initOkHttpClient()) .build() .create(IFileLoad.class) .loadFile() .enqueue(new FileCallback(destFileDir, DestFileName) {@override public void onSuccess(File File) {log.e ("zs", "request succeeded "); cancelNotification(); installApk(file); } @Override public void onLoading(long progress, long total) { Log.e("zs", progress + "----" + total); updateNotification(progress * 100 / total); } @override public void onFailure(Call Call, Throwable t) {log. e("zs", "request failed "); cancelNotification(); }}); } public interface IFileLoad { @GET("download") Call loadFile(); } /** * install software ** @param file */ private void installApk(file file) {Uri Uri = uri.fromfile (file); Intent install = new Intent(Intent.ACTION_VIEW); install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); install.setDataAndType(uri, "application/vnd.android.package-archive"); mContext.startActivity(install); } /** * initialize OkHttpClient ** @return */ private OkHttpClient initOkHttpClient() {okHttpClient.builder Builder = new OkHttpClient.Builder(); builder.connectTimeout(100000, TimeUnit.SECONDS); builder.networkInterceptors().add(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Response originalResponse = chain.proceed(chain.request()); return originalResponse .newBuilder() .body(new FileResponseBody(originalResponse)) .build(); }}); return builder.build(); } / initial Notification notice * * * * / public void initNotification () {builder = new NotificationCompat. Builder (mContext) .setSmallIcon(r.map.ic_launcher).setContentText("0%").setContentTitle("QQ Update ").setProgress(100, 0, false); notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFY_ID, builder.build()); } /** * updateNotification */ public void updateNotification(long progress) {int currProgress = (int) progress; if (preProgress < currProgress) { builder.setContentText(progress + "%"); builder.setProgress(100, (int) progress, false); notificationManager.notify(NOTIFY_ID, builder.build()); } preProgress = (int) progress; } / cancellation notice * * * * / public void cancelNotification () {notificationManager. Cancel (NOTIFY_ID); }}Copy the code
A couple of things to watch out for
-
Why download apK from onStartCommand instead of onCreate
This is related to the service lifecycle. OnStartCommand () is called every time startService is called, whereas onCreate() is used only when the service is started for the first time. Calling startService again when a service is already started will not call onCreate(), but onStartCommand() will be called again.
-
Send notifications to setProgress using setProgress, which is available only above 4.0
If you want to make ProgressBar compatible with lower versions of the ProgressBar, you can only use the custom Notification and then create a layout containing the ProgressBar. However, the custom Notification is too cumbersome to adapt to different systems. Because of different system notification bar do not match the background color, we need to do different adaptation to different background to ensure the normal order of the words can show, for example, you write dead a white text, but the system notice the background is white, so that the text can’t display properly, of course, if the system is used to inform the style can meet your need, Only custom styles can be used. Refer to this articleAndroid custom notification style ADAPTS. If only for compatibility with the lower version I personally feel completely unnecessary, you can look at the Umeng index, so there is no need to do compatibility. It depends on different needs.
-
I made a judgment call when updating the notice
I made a judgment call on the update notification and look at the code. Here I set it to update only when progress is equal to 1 and changes, because progress might be 1.1, 1.2, 1.3, but it will always show 1%. If I don’t make a judgment call then every onLoading callback will update the notification. That is, it will be updated when 1.1, and it will be updated when 1.2, but the foreground display is 1%, so it does a lot of useless work, and when you do not limit the app will be very jammed, sometimes jammed. You can run the Demo without restrictions and try it out. It’s stuck.
/** * updateNotification */ public void updateNotification(long progress) {int currProgress = (int) progress; if (preProgress < currProgress) { builder.setContentText(progress + "%"); builder.setProgress(100, (int) progress, false); notificationManager.notify(NOTIFY_ID, builder.build()); } preProgress = (int) progress; }Copy the code
conclusion
Finally, I still hope that if you have time, you can follow my steps to knock on the code and have a look at the articles I recommend. I believe that you will definitely gain a lot. If you have any good suggestions or questions I wrote, PLEASE give me feedback in time. If this article is helpful to you, you are welcome to pay more attention to me and support me. Don’t forget to go to github star and fork me, DownLoadManager source code