也是公司的项目需要,就稍微研究了下,参考网上一些不错的思路,但其适用版本都比较早,所以通知做了适配了Android 8.0,及权限问题等问题。
原理;下载apk过程中,发起一个通知,并不断发起最新进度的相同ID的通知,覆盖上一个通知,达到显示当前下载进度的效果。
demo已上传:https://download.csdn.net/download/u013370255/10603681
下面简单贴一下代码,及一些需要注意和说明的地方。
1.运行时权限:permissionsdispatcher
用法百度下,多的泛,我就不多说了,
// Permission
implementation 'com.github.hotchemi:permissionsdispatcher:3.1.0'
annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:3.1.0'
implementation 'com.android.support:support-v4:27.1.1'
同时AndroidManifast申请以下权限:
<!--允许程序设置内置sd卡的写权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--允许程序获取网络状态-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许程序访问WiFi网络信息-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--允许程序打开网络套接字-->
<uses-permission android:name="android.permission.INTERNET" />
<!--android8.0安装apk权限-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
获取文件存储权限,本当提示弹窗确认权限,这里偷个懒,直接通过了
@RuntimePermissions
public class BaseActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
BaseActivityPermissionsDispatcher.storageWithPermissionCheck(this);
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// NOTE: delegate the permission handling to generated method
BaseActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}
@NeedsPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void storage() {
//动态权限
}
@OnShowRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void showRationaleForStorage(final PermissionRequest request) {
// TODO: 2018/8/16 当提示弹窗确认权限,这里偷个懒,直接通过了
request.proceed();
}
@OnPermissionDenied(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void showDeniedForStorage() {
}
@OnNeverAskAgain(Manifest.permission.WRITE_EXTERNAL_STORAGE)
void showNeverAskForStorage() {
}
}
- MainActivity 调用,传入你的下载路径,保存的文件名
public class MainActivity extends BaseActivity {
String path = "你的下载路径";
String name = "下载后的文件名";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, UpdateService.class);
Bundle bundle = new Bundle();
bundle.putString("path",path);
bundle.putString("name",name);
intent.putExtras(bundle);
startService(intent);
}
}
3.主要服务UpdateService ,避免手机应用进程划掉就不下载了,并在AndroidManifast进行注册
<service android:name=".Download.UpdateService" />
public class UpdateService extends Service {
UpdateManagerNotification mUpdateManagerNotification;
/**
* 首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。
* 如果服务已在运行,则不会调用此方法。该方法只被调用一次
*/
@Override
public void onCreate() {
Log.i("bb","onCreate invoke");
//下载情况通知,点击通知的操作,这里表示跳转到MainActivity
mUpdateManagerNotification = new UpdateManagerNotification(getApplicationContext());
Intent intentLoGo = new Intent(getApplicationContext(), MainActivity.class);
PendingIntent piLoGo = PendingIntent.getActivity(getApplicationContext(), 0, intentLoGo, 0);
mUpdateManagerNotification.showNotification(getApplicationContext(),piLoGo,"软件更新","软件更新",getApplicationContext().getString(R.string.app_name),"软件更新",getApplicationContext().getString(R.string.app_name));
super.onCreate();
}
/**
* 绑定服务时才会调用
* 必须要实现的方法
* @param intent
* @return
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.i("bb","onBind invoke");
return null;
}
/**
* 每次通过startService()方法启动Service时都会被回调。
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Bundle bundle = intent.getExtras();
if(bundle != null){
String path = bundle.getString("path","");
String name = bundle.getString("name","");
UpdateDownloadRequest mUpdateDownloadRequest = new UpdateDownloadRequest(new UpdateDownloadListener() {
@Override
public void onStarted() {
}
@Override
public void onProgressChanged(int progress, String downloadUrl) {
Message message = new Message();
message.what = progress;
mUpdateManagerNotification.handler.sendMessage(message);
Log.i("bb","progress = " + progress);
}
@Override
public void onFinished(float completeSize, String downloadUrl) {
Message message = new Message();
message.what = 100;
mUpdateManagerNotification.handler.sendMessage(message);
}
@Override
public void onFailure(Exception e) {
Log.e("Exception:/","Update Download Exception = " + e.getMessage());
}
});
mUpdateDownloadRequest.downLoadApk(getApplication(),path,name);
}
return super.onStartCommand(intent, flags, startId);
}
/**
* 服务销毁时的回调
*/
@Override
public void onDestroy() {
Log.i("bb","onDestroy invoke");
super.onDestroy();
}
}
4.通知实现,适配Android8.0,系统布局(大布局)
这里需要注意的是通知图标和提示文字需要你自己定,并且代码中的图标logo_alpha必须是32*32的白色切图,这个好像是Google的规范,注意下。
关键代码
这句代码可以去除Android8.0的声音和震动,不加的话,即使你不设置也会有声音或震动。
channel.setSound(null, null);
public class UpdateManagerNotification {
private Context mContext;
NotificationManager notificationManager;
NotificationChannel channel;
NotificationCompat.Builder builder;
@SuppressLint ("HandlerLeak")
public Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if(msg.what == 100){//下完安装,并清除通知
//android O后必须传入NotificationChannel
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
notificationManager.cancel(Constants.NOTIFICATIONID_APP);
}else {
NotificationManagerCompat managerCompat = NotificationManagerCompat.from(mContext);
managerCompat.cancel(Constants.NOTIFICATIONID_APP);
}
}else if(msg.what >= 0 && msg.what < 100){//下载进度
builder.setProgress(100, msg.what, false);
builder.setContentText("下载进度:" + msg.what + "%");
builder.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE);
//android O后必须传入NotificationChannel
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
notificationManager.notify(Constants.NOTIFICATIONID_APP, builder.build());
}else {
NotificationManagerCompat managerCompat = NotificationManagerCompat.from(mContext);
managerCompat.notify(Constants.NOTIFICATIONID_APP, builder.build());
}
}
}
};
public UpdateManagerNotification(Context context) {
this.mContext = context;
}
/**
* 生成通知
* @param context
* @param pi
* 例如:
* Intent intentYiChe = new Intent(context, YiCheRecordActivity.class);
* Bundle bundle = new Bundle();
* intentYiChe.putExtras(bundle);
* PendingIntent piYiChe = PendingIntent.getActivity(context, 0, intentYiChe, 0);
* @param ticker 标题
* @param contentTitle 标题
* @param bigContentTitle 标题
* @param summaryText 标题
* @param Message 显示内容
*/
public void showNotification(Context context, PendingIntent pi,
String ticker, String contentTitle, String bigContentTitle, String summaryText,
String Message){
//android O后必须传入NotificationChannel
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if(notificationManager != null){
//ChannelId为"1",ChannelName为"Channel1"
channel = new NotificationChannel("4",
"运维通更新通知通道", NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(true); //是否在桌面icon右上角展示小红点
channel.setLightColor(Color.YELLOW); //小红点颜色
channel.setShowBadge(false); //是否在久按桌面图标时显示此渠道的通知
channel.setSound(null, null);
notificationManager.createNotificationChannel(channel);
builder = new NotificationCompat.Builder(context,"4");
setNotification(builder,context,pi,ticker,contentTitle,bigContentTitle,summaryText,Message);
notificationManager.notify(Constants.NOTIFICATIONID_APP, builder.build());
}
}else {
builder = new NotificationCompat.Builder(context,null);
setNotification(builder,context,pi,ticker,contentTitle,bigContentTitle,summaryText,Message);
NotificationManagerCompat managerCompat = NotificationManagerCompat.from(context);
managerCompat.notify(Constants.NOTIFICATIONID_APP, builder.build());
}
}
/**
* 设置大布局通知参数
* @param builder
* @param context
* @param pi
* @param ticker
* @param contentTitle
* @param bigContentTitle
* @param summaryText
* @param Message
*/
private void setNotification(NotificationCompat.Builder builder, Context context, PendingIntent pi,
String ticker, String contentTitle, String bigContentTitle, String summaryText,
String Message){
builder.setLargeIcon(BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher))
.setTicker(ticker)
.setContentTitle(contentTitle)
.setWhen(System.currentTimeMillis())
.setContentIntent(pi)
.setAutoCancel(false)//设置通知被点击一次是否自动取消
.setOngoing(false)
.setDefaults(NotificationCompat.FLAG_ONLY_ALERT_ONCE)
.setProgress(100, 0, false);
//大布局通知在4.1以后才能使用,BigTextStyle
NotificationCompat.BigTextStyle textStyle = null;
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
textStyle = new NotificationCompat.BigTextStyle();
textStyle.setBigContentTitle(bigContentTitle)
// 标题
.setSummaryText(summaryText)
.bigText(Message);// 内容
builder.setStyle(textStyle);
}
builder.setContentText(Message);
if(SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.setSmallIcon(R.drawable.logo_alpha);
} else {
builder.setSmallIcon(R.mipmap.ic_launcher);
}
}
}
5.接口,简单的罗列下几个阶段,方便回调
public interface UpdateDownloadListener {
void onStarted();
void onProgressChanged(int progress, String downloadUrl);
void onFinished(float completeSize, String downloadUrl);
void onFailure(Exception e);
}
6.apk下载线程与安装方法
public class UpdateDownloadRequest{
private UpdateDownloadListener listener;
public UpdateDownloadRequest(UpdateDownloadListener listener) {
this.listener = listener;
}
/**
* 下载APK文件
* @param context
* @param url
* @param filename
*/
public void downLoadApk(final Context context,final String url, final String filename) {
new Thread() {
@Override
public void run() {
try {
File file = getFileFromServer(context,url,filename);
sleep(1000);
installApk(file, context);
} catch (Exception e) {
listener.onFailure(e);
e.printStackTrace();
}
}
}.start();
}
/**
* 安装软件包
* @param file
* @param context
*/
private void installApk(File file, final Context context) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file),
"application/vnd.android.package-archive");
context.startActivity(intent);
}
/**
* 下载文件
* @param context
* @param path
* @param filename
* @return
* @throws Exception
*/
private File getFileFromServer(Context context, String path,String filename) throws Exception {// 单线程从服务器下载软件包
if (Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(10000);
long beforeTime = System.currentTimeMillis();
int count = conn.getContentLength(); //文件总大小 字节
InputStream is = conn.getInputStream();
File file = new File(PathManger.getApkDir().getAbsoluteFile(),
filename);
FileOutputStream fos = new FileOutputStream(file);
BufferedInputStream bis = new BufferedInputStream(is);
byte[] buffer = new byte[1024];
int len;
int total = 0;
while ((len = bis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
total += len;
//1秒 更新2次进度 非常重要 否则 系统会慢慢卡死
if (System.currentTimeMillis() - beforeTime > 500) {
listener.onProgressChanged((int) (((double) total / (double) count) * 100), path);
}
}
fos.close();
bis.close();
is.close();
listener.onFinished(100,path);
return file;
} else {
return null;
}
}
}
总结:
A.有些次要代码就不贴了,看demo就好了,都是一些公共的方法。
B.这里没有考虑重复下载的问题(就是下载过程中有调起了服务的问题),建议在业务逻辑上去避免吧,或者你也可以优化一下这个下载线程。