feat(notification): 添加 Android 13 通知权限和 Android 11 存储权限适配

- 添加了 Android O+ 通知渠道创建支持
- 实现了 Android 13 POST_NOTIFICATIONS 权限检查
- 添加了 Android 11+ MANAGE_EXTERNAL_STORAGE 权限适配
- 更新了 AndroidManifest 中的权限配置
- 添加了存储权限请求回调处理逻辑
This commit is contained in:
2026-07-26 04:36:51 +08:00
parent 31cee5b0f5
commit 9eca197acb
3 changed files with 220 additions and 114 deletions

View File

@@ -6,9 +6,16 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name="com.arialyy.simple.base.BaseApplication"
@@ -16,6 +23,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme.NoActionBar"
tools:ignore="GoogleAppIndexingWarning">

View File

@@ -17,11 +17,17 @@
package com.arialyy.simple;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.lifecycle.Observer;
@@ -29,6 +35,7 @@ import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.arialyy.frame.permission.OnPermissionCallback;
import com.arialyy.frame.permission.PermissionManager;
import com.arialyy.frame.util.show.T;
@@ -49,6 +56,7 @@ import com.arialyy.simple.core.upload.SFtpUploadActivity;
import com.arialyy.simple.databinding.ActivityMainBinding;
import com.arialyy.simple.modlue.CommonModule;
import com.arialyy.simple.to.NormalTo;
import java.util.ArrayList;
import java.util.List;
@@ -57,130 +65,175 @@ import java.util.List;
* 首页
*/
public class MainActivity extends BaseActivity<ActivityMainBinding> {
public static final String KEY_MAIN_DATA = "KEY_MAIN_DATA";
public static final String KEY_MAIN_DATA = "KEY_MAIN_DATA";
private static final int REQUEST_MANAGE_EXTERNAL_STORAGE = 1001;
@Override protected void init(Bundle savedInstanceState) {
super.init(savedInstanceState);
setSupportActionBar(mBar);
mBar.setTitle("Aria Demo");
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
@Override
protected void init(Bundle savedInstanceState) {
super.init(savedInstanceState);
setSupportActionBar(mBar);
mBar.setTitle("Aria Demo");
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getBinding().list.setLayoutManager(new LinearLayoutManager(this));
final List<NormalTo> data = new ArrayList<>();
final Adapter adapter = new Adapter(this, data);
getBinding().list.setAdapter(adapter);
getBinding().list.addItemDecoration(
new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
getBinding().list.setLayoutManager(new LinearLayoutManager(this));
final List<NormalTo> data = new ArrayList<>();
final Adapter adapter = new Adapter(this, data);
getBinding().list.setAdapter(adapter);
getBinding().list.addItemDecoration(
new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
final CommonModule module = ViewModelProviders.of(this).get(CommonModule.class);
module.getMainData(this).observe(this, new Observer<List<NormalTo>>() {
@Override public void onChanged(@Nullable List<NormalTo> normalTos) {
if (normalTos != null) {
data.addAll(normalTos);
adapter.notifyDataSetChanged();
}
}
});
RvItemClickSupport.addTo(getBinding().list).setOnItemClickListener(
new RvItemClickSupport.OnItemClickListener() {
@Override public void onItemClicked(RecyclerView recyclerView, int position, View v) {
switch (position) {
case 0:
module.startNextActivity(MainActivity.this, data.get(position),
DownloadActivity.class);
break;
case 1:
module.startNextActivity(MainActivity.this, data.get(position),
HttpUploadActivity.class);
break;
case 2:
module.startNextActivity(MainActivity.this, data.get(position),
DownloadGroupActivity.class);
break;
case 3:
module.startNextActivity(MainActivity.this, data.get(position),
FtpDownloadActivity.class);
break;
case 4:
module.startNextActivity(MainActivity.this, data.get(position),
FTPDirDownloadActivity.class);
break;
case 5:
module.startNextActivity(MainActivity.this, data.get(position),
FtpUploadActivity.class);
break;
case 6:
module.startNextActivity(MainActivity.this, data.get(position),
M3U8VodDLoadActivity.class);
break;
case 7:
module.startNextActivity(MainActivity.this, data.get(position),
M3U8LiveDLoadActivity.class);
break;
case 8: // d_sftp
module.startNextActivity(MainActivity.this, data.get(position),
SFtpDownloadActivity.class);
break;
case 9: // u_sftp
module.startNextActivity(MainActivity.this, data.get(position),
SFtpUploadActivity.class);
break;
final CommonModule module = ViewModelProviders.of(this).get(CommonModule.class);
module.getMainData(this).observe(this, new Observer<List<NormalTo>>() {
@Override
public void onChanged(@Nullable List<NormalTo> normalTos) {
if (normalTos != null) {
data.addAll(normalTos);
adapter.notifyDataSetChanged();
}
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean hasPermission = PermissionManager.getInstance()
.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (!hasPermission) {
PermissionManager.getInstance().requestPermission(this, new OnPermissionCallback() {
@Override public void onSuccess(String... permissions) {
}
RvItemClickSupport.addTo(getBinding().list).setOnItemClickListener(
new RvItemClickSupport.OnItemClickListener() {
@Override
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
switch (position) {
case 0:
module.startNextActivity(MainActivity.this, data.get(position),
DownloadActivity.class);
break;
case 1:
module.startNextActivity(MainActivity.this, data.get(position),
HttpUploadActivity.class);
break;
case 2:
module.startNextActivity(MainActivity.this, data.get(position),
DownloadGroupActivity.class);
break;
case 3:
module.startNextActivity(MainActivity.this, data.get(position),
FtpDownloadActivity.class);
break;
case 4:
module.startNextActivity(MainActivity.this, data.get(position),
FTPDirDownloadActivity.class);
break;
case 5:
module.startNextActivity(MainActivity.this, data.get(position),
FtpUploadActivity.class);
break;
case 6:
module.startNextActivity(MainActivity.this, data.get(position),
M3U8VodDLoadActivity.class);
break;
case 7:
module.startNextActivity(MainActivity.this, data.get(position),
M3U8LiveDLoadActivity.class);
break;
case 8: // d_sftp
module.startNextActivity(MainActivity.this, data.get(position),
SFtpDownloadActivity.class);
break;
case 9: // u_sftp
module.startNextActivity(MainActivity.this, data.get(position),
SFtpUploadActivity.class);
break;
}
}
});
@Override public void onFail(String... permissions) {
T.showShort(MainActivity.this, "没有文件读写权限");
finish();
}
}, Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE);
}
}
}
@Override protected int setLayoutId() {
return R.layout.activity_main;
}
private static class Adapter extends AbsRVAdapter<NormalTo, Adapter.Holder> {
Adapter(Context context, List<NormalTo> data) {
super(context, data);
checkPermission();
}
@Override protected Holder getViewHolder(View convertView, int viewType) {
return new Holder(convertView);
@Override
protected int setLayoutId() {
return R.layout.activity_main;
}
@Override protected int setLayoutId(int type) {
return R.layout.item_main;
/**
* Android版本适配权限检查兼容分区存储
* <ul>
* <li>Android 11+ (API 30+): 需要 {@link android.Manifest.permission#MANAGE_EXTERNAL_STORAGE},跳转系统设置页授权</li>
* <li>Android 6-10 (API 23-29): 使用传统 {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} / {@link android.Manifest.permission#READ_EXTERNAL_STORAGE} 运行时权限</li>
* <li>Android 5.x 及以下 (API < 23): 安装时授权,无需运行时处理</li>
* </ul>
*/
@SuppressLint("InlinedApi")
private void checkPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+:使用 MANAGE_EXTERNAL_STORAGE 获取所有文件访问权限
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE);
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6-10传统存储权限
boolean hasPermission = PermissionManager.getInstance()
.checkPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (!hasPermission) {
PermissionManager.getInstance().requestPermission(this, new OnPermissionCallback() {
@Override
public void onSuccess(String... permissions) {
}
@Override
public void onFail(String... permissions) {
T.showShort(MainActivity.this, "没有文件读写权限");
finish();
}
}, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE);
}
}
}
@Override protected void bindData(Holder holder, int position, NormalTo item) {
holder.title.setText(item.title);
holder.desc.setText(item.desc);
holder.image.setImageResource(item.icon);
@SuppressLint("InlinedApi")
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_MANAGE_EXTERNAL_STORAGE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
T.showShort(MainActivity.this, "需要授予所有文件管理权限");
finish();
}
}
}
}
private static class Holder extends AbsHolder {
TextView title, desc;
AppCompatImageView image;
private static class Adapter extends AbsRVAdapter<NormalTo, Adapter.Holder> {
Holder(View itemView) {
super(itemView);
title = findViewById(R.id.title);
desc = findViewById(R.id.desc);
image = findViewById(R.id.image);
}
Adapter(Context context, List<NormalTo> data) {
super(context, data);
}
@Override
protected Holder getViewHolder(View convertView, int viewType) {
return new Holder(convertView);
}
@Override
protected int setLayoutId(int type) {
return R.layout.item_main;
}
@Override
protected void bindData(Holder holder, int position, NormalTo item) {
holder.title.setText(item.title);
holder.desc.setText(item.desc);
holder.image.setImageResource(item.icon);
}
private static class Holder extends AbsHolder {
TextView title, desc;
AppCompatImageView image;
Holder(View itemView) {
super(itemView);
title = findViewById(R.id.title);
desc = findViewById(R.id.desc);
image = findViewById(R.id.image);
}
}
}
}
}

View File

@@ -16,9 +16,15 @@
package com.arialyy.simple.core.download.service;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import com.arialyy.simple.R;
/**
@@ -31,6 +37,28 @@ public class DownloadNotification {
private NotificationCompat.Builder mBuilder;
private static final int mNotifiyId = 0;
private static final String CHANNEL_ID = "aria_download_channel";
private static final String CHANNEL_NAME = "下载任务";
/**
* 在 Application.onCreate() 中调用,确保通知渠道在发通知之前已创建。
*/
public static void createNotificationChannel(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("Aria下载进度通知");
NotificationManager manager = (NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
if (manager != null) {
manager.createNotificationChannel(channel);
}
}
}
public DownloadNotification(Context context) {
mContext = context;
init();
@@ -38,17 +66,34 @@ public class DownloadNotification {
private void init() {
mManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(mContext);
// API 26+ 使用带 channelId 的构造函数
mBuilder = new NotificationCompat.Builder(mContext, CHANNEL_ID);
mBuilder.setContentTitle("Aria Download Test")
.setContentText("进度条")
.setProgress(100, 0, false)
.setSmallIcon(R.mipmap.ic_launcher);
mManager.notify(mNotifiyId, mBuilder.build());
notifyIfPermitted();
}
public void upload(int progress){
public void upload(int progress) {
if (mBuilder != null) {
mBuilder.setProgress(100, progress, false);
notifyIfPermitted();
}
}
/**
* Android 13+ 需要 POST_NOTIFICATIONS 运行时权限,缺少权限时静默跳过通知。
*/
@SuppressLint("MissingPermission")
private void notifyIfPermitted() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
return;
}
}
if (mManager != null) {
mManager.notify(mNotifiyId, mBuilder.build());
}
}