成年人的世界:
1:在成年人的世界里,是没有“崩溃”这个选项的。
2:他们需要小心翼翼地发泄,精打细算地缓解,并在最短的时间范围内恢复到正常。
3:他们不会在真正的大事面前倒下,因为知道自己身后没有人,或者,身后有人。他们只不过是在平淡的琐事中感到悲伤与愤怒。
效果图:
图一:
图二:
图三:
图四:
虽然代码很多, 我会为大家依次粘贴(按包粘贴)
项目目录:
依赖:
//retrofit2 + rxjava2 + okhttp3
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.12'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.4.0'
第一步:exception包
HttpTimeException类
/**
* 自定义错误信息,统一处理返回处理
*/
public class HttpTimeException extends RuntimeException {
public static final int NO_DATA = 0x2;
public HttpTimeException(int resultCode) {
this(getApiExceptionMessage(resultCode));
}
public HttpTimeException(String detailMessage) {
super(detailMessage);
}
/**
* 转换错误数据
*
* @param code 错误吗
* @return 错误信息
*/
private static String getApiExceptionMessage(int code) {
String message;
switch (code) {
case NO_DATA:
message = "无数据";
break;
default:
message = "error";
break;
}
return message;
}
}
RetryWhenNetworkException 类
/**
* retry条件
*/
public class RetryWhenNetworkException implements Function<Observable<? extends Throwable>, ObservableSource<?>> {
// retry次数
private int count = 3;
// 延迟
private long delay = 3000;
// 叠加延迟
private long increaseDelay = 3000;
public RetryWhenNetworkException() {
}
public RetryWhenNetworkException(int count, long delay) {
this.count = count;
this.delay = delay;
}
public RetryWhenNetworkException(int count, long delay, long increaseDelay) {
this.count = count;
this.delay = delay;
this.increaseDelay = increaseDelay;
}
@Override
public Observable<?> apply(Observable<? extends Throwable> input) {
return input.zipWith(Observable.range(1, count + 1), new BiFunction<Throwable, Integer, Wrapper>() {
@Override
public Wrapper apply(Throwable throwable, Integer integer) {
return new Wrapper(throwable, integer);
}
}).flatMap(new io.reactivex.functions.Function<Wrapper, ObservableSource<?>>() {
@Override
public ObservableSource<?> apply(Wrapper wrapper) {
if ((wrapper.throwable instanceof ConnectException
|| wrapper.throwable instanceof SocketTimeoutException
|| wrapper.throwable instanceof TimeoutException)
&& wrapper.index < count + 1) { //如果超出重试次数也抛出错误,否则默认是会进入onCompleted
Log.e("tag", "retry---->" + wrapper.index);
return Observable.timer(delay + (wrapper.index - 1) * increaseDelay, TimeUnit.MILLISECONDS);
}
return Observable.error(wrapper.throwable);
}
});
}
private class Wrapper {
private int index;
private Throwable throwable;
Wrapper(Throwable throwable, int index) {
this.index = index;
this.throwable = throwable;
}
}
}
第二步:http 包
HttpService类
/**
* service统一接口数据
*/
public interface HttpService {
//断点续传下载接口
@Streaming//大文件需要加入这个判断,防止下载过程中写入到内存中
@Headers("Content-type:application/octet-stream")
@GET
Observable<ResponseBody> download(@Header("RANGE") String start, @Url String url);
}
第三步 httpdowonload包
DownloadInterceptor类
/**
* 成功回调处理
* Created by pc12 on 2019/7/31.
*/
public class DownloadInterceptor implements Interceptor {
private DownloadProgressListener listener;
public DownloadInterceptor(DownloadProgressListener listener) {
this.listener = listener;
}
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadResponseBody(originalResponse.body(), listener))
.build();
}
}
DownloadProgressListener 类
/**
* 成功回调处理
* Created by pc12 on 2019/7/31.
*/
public interface DownloadProgressListener {
/**
* 下载进度
* @param read 进度
* @param count 总长度
*/
void update(long read, long count, boolean done);
}
DownloadResponseBody 类
/**
* 自定义进度的body
* Created by pc12 on 2019/7/31.
*/
public class DownloadResponseBody extends ResponseBody {
private ResponseBody responseBody;
private DownloadProgressListener progressListener;
private BufferedSource bufferedSource;
DownloadResponseBody(ResponseBody responseBody, DownloadProgressListener progressListener) {
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override
public long read(@NonNull Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted.
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
if (null != progressListener) {
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1);
}
return bytesRead;
}
};
}
}
DownInfo类
public class DownInfo {
//存储位置
private String savePath;
//下载url
private String url;
//基础url
private String baseUrl;
//文件总长度
private long countLength;
//下载长度
private long readLength;
//下载唯一的HttpService
private HttpService service;
//回调监听
//private HttpProgressOnNextListener listener;
//超时设置
private int DEFAULT_TIMEOUT = 6;
//下载状态
private DownState state;
//public DownInfo(String url,HttpProgressOnNextListener listener) {
//setUrl(url);
//setBaseUrl(getBasUrl(url));
// setListener(listener);
//}
public DownState getState() {
return state;
}
public void setState(DownState state) {
this.state = state;
}
public DownInfo(String url) {
setUrl(url);
setBaseUrl(getBasUrl(url));
}
public DownInfo() {
}
public int getConnectionTime() {
return DEFAULT_TIMEOUT;
}
public void setConnectionTime(int DEFAULT_TIMEOUT) {
this.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
}
//public HttpProgressOnNextListener getListener() {
//return listener;
//}
//
//public void setListener(HttpProgressOnNextListener listener) {
//this.listener = listener;
//}
public HttpService getService() {
return service;
}
public void setService(HttpService service) {
this.service = service;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
setBaseUrl(getBasUrl(url));
}
public String getSavePath() {
return savePath;
}
public void setSavePath(String savePath) {
this.savePath = savePath;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public long getCountLength() {
return countLength;
}
public void setCountLength(long countLength) {
this.countLength = countLength;
}
public long getReadLength() {
return readLength;
}
public void setReadLength(long readLength) {
this.readLength = readLength;
}
/**
* 读取baseurl
*/
private String getBasUrl(String url) {
String head = "";
int index = url.indexOf("://");
if (index != -1) {
head = url.substring(0, index + 3);
url = url.substring(index + 3);
}
index = url.indexOf("/");
if (index != -1) {
url = url.substring(0, index + 1);
}
return head + url;
}
}
DownState类
/**
* 下载状态
*/
public enum DownState {
START,
DOWN,
PAUSE,
STOP,
ERROR,
FINISH,
}
HttpDownManager类
/**
* http下载处理类
* Created by pc12 on 2019/7/31.
*/
public class HttpDownManager {
//记录下载数据
private Set<DownInfo> downInfos;
//回调sub队列
private HashMap<String, ProgressDownSubscriber> subMap;
//进度监听队列
private HashMap<String, HttpProgressOnNextListener<DownInfo>> mProgressListenerHashMap;
//单利对象
private volatile static HttpDownManager INSTANCE;
private HttpDownManager() {
downInfos = new HashSet<>();
subMap = new HashMap<>();
mProgressListenerHashMap = new HashMap<>();
}
/**
* 获取单例
*/
public static HttpDownManager getInstance() {
if (INSTANCE == null) {
synchronized (HttpDownManager.class) {
if (INSTANCE == null) {
INSTANCE = new HttpDownManager();
}
}
}
return INSTANCE;
}
/**
* 继续下载
*/
public void continueDownload(DownInfo downInfo) {
//根据下载的url获取到进度监听队列中的监听器
HttpProgressOnNextListener<DownInfo> httpProgressOnNextListener = mProgressListenerHashMap.get(downInfo.getUrl());
if (httpProgressOnNextListener != null) {
startDown(downInfo, httpProgressOnNextListener);
}
}
/**
* 开始下载
*/
public void startDown(final DownInfo info, HttpProgressOnNextListener<DownInfo> httpProgressOnNextListener) {
//正在下载返回异常信息
if (info.getState() == DownState.DOWN) {
httpProgressOnNextListener.onError(new Exception("正在下载中"));
return;
}
//添加回调处理类
ProgressDownSubscriber<DownInfo> subscriber = new ProgressDownSubscriber<>(info, httpProgressOnNextListener);
//将监听进度的器添加到队列中,以便于继续下载时获取
mProgressListenerHashMap.put(info.getUrl(), httpProgressOnNextListener);
//记录回调sub
subMap.put(info.getUrl(), subscriber);
//获取service,多次请求公用一个service
HttpService httpService;
//设置文件的状态为下载
info.setState(DownState.START);
if (downInfos.contains(info)) {
httpService = info.getService();
} else {
DownloadInterceptor interceptor = new DownloadInterceptor(subscriber);
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//手动创建一个OkHttpClient并设置超时时间
builder.connectTimeout(info.getConnectionTime(), TimeUnit.SECONDS);
builder.addInterceptor(interceptor);
Retrofit retrofit = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(info.getBaseUrl())
.build();
httpService = retrofit.create(HttpService.class);
info.setService(httpService);
downInfos.add(info);
}
//得到rx对象-上一次下载的位置开始下载
httpService.download("bytes=" + info.getReadLength() + "-", info.getUrl())
//指定线程
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
//失败后的retry配置
.retryWhen(new RetryWhenNetworkException())
.map(new Function<ResponseBody, DownInfo>() {
@Override
public DownInfo apply(ResponseBody responseBody) {
try {
writeCache(responseBody, new File(info.getSavePath()), info);
} catch (IOException e) {
//失败抛出异常
throw new HttpTimeException(e.getMessage());
}
return info;
}
})//回调线程
.observeOn(AndroidSchedulers.mainThread())
//数据回调
.subscribe(subscriber);
}
/**
* 停止下载
*/
public void stopDown(DownInfo info) {
if (info == null) return;
info.setState(DownState.STOP);
if (subMap.containsKey(info.getUrl())) {
ProgressDownSubscriber subscriber = subMap.get(info.getUrl());
subscriber.unSubscribe();
subMap.remove(info.getUrl());
}
}
/**
* 删除
*/
@SuppressWarnings("unused")
public void deleteDown(DownInfo info) {
stopDown(info);
}
/**
* 暂停下载
*/
public void pause(DownInfo info) {
if (info == null) return;
info.setState(DownState.PAUSE);
if (subMap.containsKey(info.getUrl())) {
ProgressDownSubscriber subscriber = subMap.get(info.getUrl());
subscriber.unSubscribe();
subMap.remove(info.getUrl());
}
}
/**
* 停止全部下载
*/
@SuppressWarnings("unused")
public void stopAllDown() {
for (DownInfo downInfo : downInfos) {
stopDown(downInfo);
}
subMap.clear();
downInfos.clear();
}
/**
* 暂停全部下载
*/
@SuppressWarnings("unused")
public void pauseAll() {
for (DownInfo downInfo : downInfos) {
pause(downInfo);
}
subMap.clear();
downInfos.clear();
}
/**
* 返回全部正在下载的数据
*/
@SuppressWarnings("unused")
public Set<DownInfo> getDownInfos() {
return downInfos;
}
/**
* 写入文件
*/
private void writeCache(ResponseBody responseBody, File file, DownInfo info) throws IOException {
if (!file.getParentFile().exists()) {
boolean bol = file.getParentFile().mkdirs();
if (!bol) {
Log.e("TAG", "文件创建失败");
}
}
long allLength;
if (info.getCountLength() == 0) {
allLength = responseBody.contentLength();
} else {
allLength = info.getCountLength();
}
FileChannel channelOut;
RandomAccessFile randomAccessFile;
randomAccessFile = new RandomAccessFile(file, "rwd");
channelOut = randomAccessFile.getChannel();
MappedByteBuffer mappedBuffer = channelOut.map(FileChannel.MapMode.READ_WRITE,
info.getReadLength(), allLength - info.getReadLength());
byte[] buffer = new byte[1024 * 8];
int len;
while ((len = responseBody.byteStream().read(buffer)) != -1) {
mappedBuffer.put(buffer, 0, len);
}
responseBody.byteStream().close();
channelOut.close();
randomAccessFile.close();
}
}
ProgressDownSubscriberl类
/**
* 用于在Http请求开始时,自动显示一个ProgressDialog
* 在Http请求结束是,关闭ProgressDialog
* 调用者自己对请求数据进行处理
*/
public class ProgressDownSubscriber<T> implements Observer<T>, DownloadProgressListener {
//弱引用结果回调
private WeakReference<HttpProgressOnNextListener<T>> mHttpProgressOnNextListener;
/*下载数据*/
private DownInfo downInfo;
private Disposable mDisposable;
public ProgressDownSubscriber(DownInfo downInfo, HttpProgressOnNextListener<T> httpProgressOnNextListener) {
this.mHttpProgressOnNextListener = new WeakReference<>(httpProgressOnNextListener);
this.downInfo = downInfo;
}
public void unSubscribe() {
if (mDisposable != null) {
mDisposable.dispose();
}
}
@Override
public void onSubscribe(Disposable disposable) {
mDisposable = disposable;
}
@Override
public void onNext(T t) {
if (mHttpProgressOnNextListener.get() != null) {
mHttpProgressOnNextListener.get().onNext(t);
}
}
/**
* 对错误进行统一处理
* 隐藏ProgressDialog
*
* @param e 异常
*/
@Override
public void onError(Throwable e) {
mDisposable.dispose();
/*停止下载*/
HttpDownManager.getInstance().stopDown(downInfo);
if (mHttpProgressOnNextListener.get() != null) {
mHttpProgressOnNextListener.get().onError(e);
}
downInfo.setState(DownState.ERROR);
}
/**
* 完成,隐藏ProgressDialog
*/
@Override
public void onComplete() {
mDisposable.dispose();
if (mHttpProgressOnNextListener.get() != null) {
mHttpProgressOnNextListener.get().onComplete();
}
downInfo.setState(DownState.FINISH);
}
@SuppressLint("CheckResult")
@Override
public void update(long read, long count, boolean done) {
if (downInfo.getCountLength() > count) {
read = downInfo.getCountLength() - count + read;
} else {
downInfo.setCountLength(count);
}
downInfo.setReadLength(read);
if (mHttpProgressOnNextListener.get() != null) {
/*接受进度消息,造成UI阻塞,如果不需要显示进度可去掉实现逻辑,减少压力*/
Observable.just(read).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) {
/*如果暂停或者停止状态延迟,不需要继续发送回调,影响显示*/
if (downInfo.getState() == DownState.PAUSE || downInfo.getState() == DownState.STOP)
return;
downInfo.setState(DownState.DOWN);
mHttpProgressOnNextListener.get().updateProgress(aLong, downInfo.getCountLength());
}
});
}
}
}
第四步listener 包
HttpProgressOnNextListener类
/**
* 下载过程中的回调处理
*/
public abstract class HttpProgressOnNextListener<T> {
/**
* 成功后回调方法
*
* @param t 下载成功之后文件信息
*/
public abstract void onNext(T t);
/**
* 完成下载
* 主动调用,更加灵活
*/
public void onComplete(){
}
/**
* 下载进度
*
* @param readLength 当前下载进度
* @param countLength 文件总长度
*/
public abstract void updateProgress(long readLength, long countLength);
/**
* 失败或者错误方法
* 主动调用,更加灵活
*
* @param e 下载失败异常
*/
public void onError(Throwable e) {
}
///**
//* 开始下载
// */
//public abstract void onStart();
/ /**
// * 暂停下载
// */
// public void onPuase() {
//
// }
//
// /**
// * 停止下载销毁
// */
// public void onStop() {
//
// }
}
第五步:entity包
VersionInfo 类
/**
* 版本更新实体类
*/
public class VersionInfo implements Serializable {
private String downloadUrl;
private Integer versionCode;
private String versionName;
private String updateMessage;
public String getDownloadUrl() {
return downloadUrl;
}
public void setDownloadUrl(String downloadUrl) {
this.downloadUrl = downloadUrl;
}
public Integer getVersionCode() {
return versionCode;
}
public void setVersionCode(Integer versionCode) {
this.versionCode = versionCode;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getUpdateMessage() {
return updateMessage;
}
public void setUpdateMessage(String updateMessage) {
this.updateMessage = updateMessage;
}
}
第六步:request 包
ApiService 类
public interface ApiService {
/**
* 版本检测
*/
@GET(Constants.UPDATE_URL)
Observable<Response<VersionInfo>> checkVersion();
}
OkHttpUtils 类
public class OkHttpUtils {
/**
* okhttp
*/
private static OkHttpClient okHttpClient;
/**
* Retrofit
*/
private static Retrofit retrofit;
/**
* 获取Retrofit的实例
*
* @return retrofit
*/
public static Retrofit getRetrofit() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl("https://raw.githubusercontent.com/wj576038874/mvp-rxjava-retrofit-okhttp/master/")
.addConverterFactory(GsonConverterFactory.create())
.client(getOkHttpClient())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
}
return retrofit;
}
private static OkHttpClient getOkHttpClient() {
if (okHttpClient == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(15 * 1000, TimeUnit.SECONDS);
builder.readTimeout(15 * 1000, TimeUnit.MILLISECONDS);//超时时间
okHttpClient = builder.build();
}
return okHttpClient;
}
}
RequestSubscriber类
/**
* 请求被订阅者
* @param <T>
*/
public abstract class RequestSubscriber<T> implements Observer<T> {
/**
* 定义一个请求成功的抽象方法 子类必须实现并在实现中进行处理服务器返回的数据
*
* @param t 服务器返回的数据
*/
protected abstract void onSuccess(T t);
/**
* 定义一个请求失败的抽象方法 子类必须实现并在实现中进行服务器返回数据的处理
*
* @param msg 服务器返回的错误信息
*/
protected abstract void onFailure(String msg);
@Override
public void onSubscribe(Disposable d) {
}
@Override
public void onNext(T t) {
/*
* 请求成功将数据发出去
*/
onSuccess(t);
}
@Override
public void onError(Throwable e) {
String msg;
if (e instanceof SocketTimeoutException) {
msg = "请求超时。请稍后重试!";
} else if (e instanceof ConnectException) {
msg = "请求超时。请稍后重试!";
} else {
msg = "请求未能成功,请稍后重试!";
}
if (!TextUtils.isEmpty(msg)) {
onFailure(msg);
}
}
@Override
public void onComplete() {
}
}
第七步: service 包
DownloadService类
/**
* 下载服务
*/
public class DownloadService extends Service {
private DownInfo downInfo;
private int oldProgress = 0;
private NotificationHelper notificationHelper;
@Override
public void onCreate() {
super.onCreate();
downInfo = new DownInfo();
notificationHelper = new NotificationHelper(this);
}
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
assert intent != null;
String urlStr = intent.getStringExtra(Constants.APK_DOWNLOAD_URL);
downInfo.setUrl(urlStr);
File dir = StorageUtils.getExternalCacheCustomDirectory(this);
//File dir = StorageUtils.getExternalCacheDirectory(this);
//File dir = StorageUtils.getCacheDirectory(this);
String apkName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.length());
File apkFile = new File(dir, apkName);
downInfo.setSavePath(apkFile.getAbsolutePath());
downLoadFile();
return super.onStartCommand(intent, flags, startId);
}
private void downLoadFile() {
HttpDownManager.getInstance().startDown(downInfo, new HttpProgressOnNextListener<DownInfo>() {
@Override
public void onNext(DownInfo downInfo) {
//收起通知栏
NotificationBarUtil.setNotificationBarVisibility(DownloadService.this, false);
//安装
ApkUtils.installAPk(DownloadService.this, new File(downInfo.getSavePath()));
}
@Override
public void onComplete() {
notificationHelper.cancel();
stopSelf();
}
@Override
public void updateProgress(long readLength, long countLength) {
int progress = (int) (readLength * 100 / countLength);
// 如果进度与之前进度相等,则不更新,如果更新太频繁,否则会造成界面卡顿
if (progress != oldProgress) {
notificationHelper.updateProgress(progress);
}
oldProgress = progress;
}
@Override
public void onError(Throwable e) {
Toast.makeText(DownloadService.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
第八步:utils 包
ApkUtils 类
public class ApkUtils {
/**
* 安装apk
* @param context context
* @param apkFile 安装文件
*/
public static void installAPk(Context context, File apkFile) {
Intent installAPKIntent = getApkInStallIntent(context, apkFile);
context.startActivity(installAPKIntent);
}
/**
* 获取安装文件意图
* @param context context
* @param apkFile 安装文件
* @return 安装意图
*/
private static Intent getApkInStallIntent(Context context, File apkFile) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
Uri uri = FileProvider.getUriForFile(context, "com.winfo.update.provider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
} else {
Uri uri = getApkUri(apkFile);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
}
return intent;
}
/**
* 获取安装文件的Uri
* @param apkFile 安装文件
* @return Uri
*/
private static Uri getApkUri(File apkFile) {
//如果没有设置 SDCard 写权限,或者没有 SDCard,apk 文件保存在内存中,需要授予权限才能安装
try {
String[] command = {"chmod", "777", apkFile.toString()};
ProcessBuilder builder = new ProcessBuilder(command);
builder.start();
} catch (IOException ignored) {
}
return Uri.fromFile(apkFile);
}
}
AppUtils 类
public class AppUtils {
/**
* 获取当前应用的版本号
* @param mContext context
* @return 版本号
*/
public static int getVersionCode(Context mContext) {
if (mContext != null) {
try {
return mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException ignored) {
}
}
return 0;
}
/**
* 获取当前应用的版本名称
* @param mContext context
* @return 版本名称
*/
public static String getVersionName(Context mContext) {
if (mContext != null) {
try {
return mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException ignored) {
}
}
return "";
}
}
Constants类
public class Constants {
public static final String APK_DOWNLOAD_URL = "downloadUrl";
public static final String UPDATE_URL = "version.json";
}
NotificationBarUtil类
public class NotificationBarUtil {
/**
* 设置通知栏是否收起
* @param context context
* @param isVisible 是否收起
*/
@RequiresPermission(EXPAND_STATUS_BAR)
public static void setNotificationBarVisibility(Context context, boolean isVisible) {
String methodName;
if (isVisible) {
methodName = (Build.VERSION.SDK_INT <= 16) ? "expand" : "expandNotificationsPanel";
} else {
methodName = (Build.VERSION.SDK_INT <= 16) ? "collapse" : "collapsePanels";
}
invokePanels(context, methodName);
}
private static void invokePanels(Context context, String methodName) {
try {
@SuppressLint("WrongConstant")
Object service = context.getSystemService("statusbar");
@SuppressLint("PrivateApi")
Class<?> statusBarManager = Class.forName("android.app.StatusBarManager");
Method expand = statusBarManager.getMethod(methodName);
expand.invoke(service);
} catch (Exception e) {
e.printStackTrace();
}
}
}
NotificationHelper类
public class NotificationHelper {
private NotificationManager manager;
private Context mContext;
private static String CHANNEL_ID = "dxy_app_update";
private static final int NOTIFICATION_ID = 0;
public NotificationHelper(Context context) {
this.mContext = context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID, "应用更新", NotificationManager.IMPORTANCE_NONE);
mChannel.setDescription("应用有新版本");
mChannel.enableLights(true); //是否在桌面icon右上角展示小红点
mChannel.setShowBadge(true);
getManager().createNotificationChannel(mChannel);
}
}
/**
* 显示Notification
*/
public void showNotification(String content, String apkUrl) {
Intent myIntent = new Intent(mContext, DownloadService.class);
myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
myIntent.putExtra(Constants.APK_DOWNLOAD_URL, apkUrl);
PendingIntent pendingIntent = PendingIntent.getService(mContext, 0, myIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = getNofity(content)
.setContentIntent(pendingIntent);
getManager().notify(NOTIFICATION_ID, builder.build());
}
/**
* 不断调用次方法通知通知栏更新进度条
*/
public void updateProgress(int progress) {
String text = mContext.getString(R.string.android_auto_update_download_progress, progress);
PendingIntent pendingintent = PendingIntent.getActivity(mContext, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder = getNofity(text)
.setProgress(100, progress, false)
.setContentIntent(pendingintent);
getManager().notify(NOTIFICATION_ID, builder.build());
}
private NotificationCompat.Builder getNofity(String text) {
return new NotificationCompat.Builder(mContext.getApplicationContext(), CHANNEL_ID)
.setTicker(mContext.getString(R.string.android_auto_update_notify_ticker))
.setContentTitle("版本更新")
.setContentText(text)
.setSmallIcon(R.mipmap.ic_launcher)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setWhen(System.currentTimeMillis())
.setPriority(NotificationCompat.PRIORITY_HIGH);
}
public void cancel() {
getManager().cancel(NOTIFICATION_ID);
}
private NotificationManager getManager() {
if (manager == null) {
manager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
return manager;
}
}
StorageUtils类
public class StorageUtils {
/*
* context.getCacheDir()和context.getExternalCacheDir()
* 目录的路径不同。
* 前者的目录存在外部SD卡上的。在手机里可以直接看到
* 后者的目录存在app的内部存储上,需要root以后,用Root Explorer 文件管理器才能看到
*/
/**
* 获取应用的缓存目录
* 路径需要root以后,用Root Explorer 文件管理器才能看到
*/
public static File getCacheDirectory(Context context) {
File appCacheDir = context.getCacheDir();
if (appCacheDir == null) {
Log.w("StorageUtils", "Can't define system cache directory! The app should be re-installed.");
}
return appCacheDir;
}
/**
* 获取应用的缓存目录 路径在手机里可以直接看到
* apk下载路径为:SDCard/Android/data/com.winfo.update/cache/
*/
public static File getExternalCacheDirectory(Context context) {
File appCacheDir = context.getExternalCacheDir();
if (appCacheDir == null) {
Log.w("StorageUtils", "Can't define system cache directory! The app should be re-installed.");
}
return appCacheDir;
}
/**
* 在cache下新增自定义缓存路径
* apk下载路径为:SDCard/Android/data/com.winfo.update/cache/update_file/
*/
public static File getExternalCacheCustomDirectory(Context context) {
//在SDCard/Android/data/com.winfo.update/cache/update_file创建文件夹
File appCacheDir = new File(context.getExternalCacheDir(), "update");
//如果不存在就创建
if (!appCacheDir.exists()) {
if (appCacheDir.mkdirs()) {//创建成功就返回SDCard/Android/data/com.winfo.update/cache/update_file/
return appCacheDir;
} else {
//创建失败就返回默认的SDCard/Android/data/com.winfo.update/cache/
return context.getExternalCacheDir();
}
} else {
//存在直接返回
return appCacheDir;
}
}
}
UpdateChecker类
/**
* 对话框检测
*
* @param mContext context
* @param dialog 加载框
*/
public static void checkForDialog(final Context mContext, final Dialog dialog) {
dialog.show();
Observable<Response<VersionInfo>> observable = OkHttpUtils.getRetrofit().create(ApiService.class).checkVersion();
Observer<Response<VersionInfo>> observer = new RequestSubscriber<Response<VersionInfo>>() {
@Override
protected void onSuccess(Response<VersionInfo> versionInfoResponse) {
dialog.dismiss();
if (versionInfoResponse.isSuccessful()) {
VersionInfo versionInfo = versionInfoResponse.body();
if (versionInfo != null) {
int apkCode = versionInfo.getVersionCode();
int versionCode = AppUtils.getVersionCode(mContext);
if (apkCode > versionCode) {
UpdateDialog.show(mContext, versionInfo.getUpdateMessage(), versionInfo.getDownloadUrl());
} else {
Toast.makeText(mContext, mContext.getString(R.string.android_auto_update_toast_no_new_update), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, "请求失败了", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, "请求失败了", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onFailure(String msg) {
dialog.dismiss();
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
};
observable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer);
}
/**
* 通知栏通知
*
* @param mContext context
* @param dialog 加载框
*/
public static void checkForNotification(final Context mContext, final Dialog dialog) {
dialog.show();
Observable<Response<VersionInfo>> observable = OkHttpUtils.getRetrofit().create(ApiService.class).checkVersion();
Observer<Response<VersionInfo>> observer = new RequestSubscriber<Response<VersionInfo>>() {
@Override
protected void onSuccess(Response<VersionInfo> versionInfoResponse) {
dialog.dismiss();
if (versionInfoResponse.isSuccessful()) {
VersionInfo versionInfo = versionInfoResponse.body();
if (versionInfo != null) {
int apkCode = versionInfo.getVersionCode();
int versionCode = AppUtils.getVersionCode(mContext);
if (apkCode > versionCode) {
new NotificationHelper(mContext).showNotification(versionInfo.getUpdateMessage(), versionInfo.getDownloadUrl());
} else {
Toast.makeText(mContext, mContext.getString(R.string.android_auto_update_toast_no_new_update), Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, "请求没有成功", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(mContext, "请求没有成功", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onFailure(String msg) {
dialog.dismiss();
Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
}
};
observable.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer);
}
}
UpdateDialog类
/**
* 弹出对话框提示更新信息,可自定义
*/
public class UpdateDialog {
/**
* 显示对话框
*
* @param context context
* @param content 更新内容
* @param downloadUrl apk下载地址
*/
public static void show(final Context context, String content, final String downloadUrl) {
if (isContextValid(context)) {
new AlertDialog.Builder(context)
.setTitle(R.string.android_auto_update_dialog_title)
.setMessage(content)
.setPositiveButton(R.string.android_auto_update_dialog_btn_download, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
goToDownload(context, downloadUrl);
}
})
.setNegativeButton(R.string.android_auto_update_dialog_btn_cancel, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
dialog.dismiss();
}
})
.setCancelable(false)
.show();
}
}
/**
* 检测context是否是Activity
*
* @param context 上下文
* @return 是否
*/
private static boolean isContextValid(Context context) {
return context instanceof Activity && !((Activity) context).isFinishing();
}
/**
* 启动服务传递下载地址进行下载
*
* @param context activity
* @param downloadUrl 下载地址
*/
private static void goToDownload(Context context, String downloadUrl) {
Intent intent = new Intent(context.getApplicationContext(), DownloadService.class);
intent.putExtra(Constants.APK_DOWNLOAD_URL, downloadUrl);
context.startService(intent);
}
}
第九步 xml
<?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:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_startDown_qq"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="下载QQ" />
<Button
android:id="@+id/btn_pauseDown_qq"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:tag="true"
android:text="暂停下载" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/progressBar_qq"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_text_qq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="0%" />
</LinearLayout>
<TextView
android:id="@+id/tv_msg_qq"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_startDown_alipay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="下载支付宝" />
<Button
android:id="@+id/btn_pauseDown_alipay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:tag="true"
android:text="暂停下载" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/progressBar_alipay"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_text_alipay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="0%" />
</LinearLayout>
<TextView
android:id="@+id/tv_msg_alipay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_startDown_weixin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="下载微信" />
<Button
android:id="@+id/btn_pauseDown_weixin"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:tag="true"
android:text="暂停下载" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/progressBar_weixin"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_text_weixin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="0%" />
</LinearLayout>
<TextView
android:id="@+id/tv_msg_weixin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:onClick="updateDialog"
android:text="对话框版本更新" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="updateNotification"
android:text="通知栏版本更新" />
</LinearLayout>
最后一步 MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private ProgressBar qqProgressBar, alipayProgressBar, weixinProgressbar;
private TextView tvQQMsg, tvAlipayMsg, tvWeixinMsg;
private TextView tvQQProgress, tvAlipayProgress, tvWeixinProgress;
private Button btnQQStart, btnQQPause, btnAlipayStart, btnAlipayPause, btnWeixinStart, btnWeixinPause;
private DownInfo qqDownInfo, alipayDownInfo, weixinDownInfo;
private ProgressDialog dialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dialog = new ProgressDialog(this);
dialog.setMessage(getString(R.string.android_auto_update_dialog_checking));
qqProgressBar = findViewById(R.id.progressBar_qq);
alipayProgressBar = findViewById(R.id.progressBar_alipay);
weixinProgressbar = findViewById(R.id.progressBar_weixin);
tvQQProgress = findViewById(R.id.tv_text_qq);
tvAlipayProgress = findViewById(R.id.tv_text_alipay);
tvWeixinProgress = findViewById(R.id.tv_text_weixin);
tvQQMsg = findViewById(R.id.tv_msg_qq);
tvAlipayMsg = findViewById(R.id.tv_msg_alipay);
tvWeixinMsg = findViewById(R.id.tv_msg_weixin);
btnQQStart = findViewById(R.id.btn_startDown_qq);
btnQQPause = findViewById(R.id.btn_pauseDown_qq);
btnAlipayStart = findViewById(R.id.btn_startDown_alipay);
btnAlipayPause = findViewById(R.id.btn_pauseDown_alipay);
btnWeixinStart = findViewById(R.id.btn_startDown_weixin);
btnWeixinPause = findViewById(R.id.btn_pauseDown_weixin);
btnQQStart.setOnClickListener(this);
btnQQPause.setOnClickListener(this);
btnAlipayStart.setOnClickListener(this);
btnAlipayPause.setOnClickListener(this);
btnWeixinStart.setOnClickListener(this);
btnWeixinPause.setOnClickListener(this);
String weixinDwonloadUrl = "http://dldir1.qq.com/weixin/android/weixin667android1320.apk";
weixinDownInfo = new DownInfo(weixinDwonloadUrl);
String weixinApkName = weixinDwonloadUrl.substring(weixinDwonloadUrl.lastIndexOf("/") + 1, weixinDwonloadUrl.length());
File weixinApkFile = new File(getExternalCacheDir(), weixinApkName);
weixinDownInfo.setSavePath(weixinApkFile.getAbsolutePath());
weixinDownInfo.setState(DownState.START);
String qqDwonloadUrl = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";
qqDownInfo = new DownInfo(qqDwonloadUrl);
String qqApkName = qqDwonloadUrl.substring(qqDwonloadUrl.lastIndexOf("/") + 1, qqDwonloadUrl.length());
File qqApkFile = new File(getExternalCacheDir(), qqApkName);
qqDownInfo.setSavePath(qqApkFile.getAbsolutePath());
qqDownInfo.setState(DownState.START);
String alipayDwonloadUrl = "http://gdown.baidu.com/data/wisegame/87b1af6e50012cb5/zhifubao_128.apk";
alipayDownInfo = new DownInfo(alipayDwonloadUrl);
String alipayApkName = alipayDwonloadUrl.substring(alipayDwonloadUrl.lastIndexOf("/") + 1, alipayDwonloadUrl.length());
File alipayApkFile = new File(getExternalCacheDir(), alipayApkName);
alipayDownInfo.setSavePath(alipayApkFile.getAbsolutePath());
alipayDownInfo.setState(DownState.START);
}
@SuppressLint("SetTextI18n")
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_startDown_qq:
HttpDownManager.getInstance().startDown(qqDownInfo, new HttpProgressOnNextListener<DownInfo>() {
@Override
public void onNext(DownInfo downInfo) {
tvQQMsg.setText("QQ下载完成" + downInfo.getSavePath());
}
@Override
public void updateProgress(long readLength, long countLength) {
qqProgressBar.setMax((int) countLength);
qqProgressBar.setProgress((int) readLength);
tvQQProgress.setText(readLength * 100 / countLength + "%");
}
@Override
public void onError(Throwable e) {
tvQQMsg.setText(e.getMessage());
}
});
break;
case R.id.btn_pauseDown_qq:
String tag = (String) btnQQPause.getTag();
if (tag.equals("true")) {
btnQQPause.setText("继续下载");
btnQQPause.setTag("false");
HttpDownManager.getInstance().pause(qqDownInfo);
} else {
btnQQPause.setText("暂停下载");
btnQQPause.setTag("true");
HttpDownManager.getInstance().continueDownload(qqDownInfo);
}
break;
case R.id.btn_startDown_alipay:
HttpDownManager.getInstance().startDown(alipayDownInfo, new HttpProgressOnNextListener<DownInfo>() {
@Override
public void onNext(DownInfo downInfo) {
tvAlipayMsg.setText("下载完成" + downInfo.getSavePath());
}
@Override
public void updateProgress(long readLength, long countLength) {
alipayProgressBar.setMax((int) countLength);
alipayProgressBar.setProgress((int) readLength);
tvAlipayProgress.setText(readLength * 100 / countLength + "%");
}
@Override
public void onError(Throwable e) {
tvAlipayMsg.setText(e.getMessage());
}
});
break;
case R.id.btn_pauseDown_alipay:
String tag1 = (String) btnAlipayPause.getTag();
if (tag1.equals("true")) {
btnAlipayPause.setText("继续下载");
btnAlipayPause.setTag("false");
HttpDownManager.getInstance().pause(alipayDownInfo);
} else {
btnAlipayPause.setText("暂停下载");
btnAlipayPause.setTag("true");
HttpDownManager.getInstance().continueDownload(alipayDownInfo);
}
break;
case R.id.btn_startDown_weixin:
HttpDownManager.getInstance().startDown(weixinDownInfo, new HttpProgressOnNextListener<DownInfo>() {
@Override
public void onNext(DownInfo downInfo) {
tvWeixinMsg.setText("下载完成" + downInfo.getSavePath());
}
@Override
public void updateProgress(long readLength, long countLength) {
weixinProgressbar.setMax((int) countLength);
weixinProgressbar.setProgress((int) readLength);
tvWeixinProgress.setText(readLength * 100 / countLength + "%");
}
@Override
public void onError(Throwable e) {
tvWeixinMsg.setText(e.getMessage());
}
});
break;
case R.id.btn_pauseDown_weixin:
String tag2 = (String) btnWeixinPause.getTag();
if (tag2.equals("true")) {
btnWeixinPause.setText("继续下载");
btnWeixinPause.setTag("false");
HttpDownManager.getInstance().pause(weixinDownInfo);
} else {
btnWeixinPause.setText("暂停下载");
btnWeixinPause.setTag("true");
HttpDownManager.getInstance().continueDownload(weixinDownInfo);
}
break;
}
}
public void updateDialog(View view) {
UpdateChecker.checkForDialog(this, dialog);
}
public void updateNotification(View view) {
UpdateChecker.checkForNotification(this, dialog);
}
}
配置问件
strings 中
<resources>
<string name="app_name">DownloadUpdateDemo</string>
<string name="android_auto_update_dialog_title">发现新版本</string>
<string name="android_auto_update_notify_ticker">发现新版本,点击进行升级</string>
<string name="android_auto_update_notify_content">发现新版本,点击进行升级</string>
<string name="android_auto_update_dialog_btn_download">立即下载</string>
<string name="android_auto_update_dialog_btn_cancel">以后再说</string>
<string name="android_auto_update_toast_no_new_update">已经是最新版本</string>
<string name="android_auto_update_download_progress">正在下载:%1$d%%</string>
<string name="android_auto_update_dialog_checking">正在检查版本</string>
</resources>
style中
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
res下 xml 包中的update_apk_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache-path" path="." /><!--name自定义 .为根路径-->
<external-cache-path name="external-cache-path" path="." /><!--name自定义 .为SDCard/Android/data/应用包名/cache/-->
<!--name自定义 update_file为SDCard/Android/data/应用包名/cache/update/和gettExternalCacheDirectory对应创建的文件夹保持一致-->
<external-cache-path name="external-cache--custom-path" path="update" />
</paths>
最最后一步:清单问件中
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.winfo.update">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR" />
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".version_update.service.DownloadService"
android:exported="false" />
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.winfo.update.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/update_apk_paths" />
</provider>
</application>
</manifest>