因为项目在华为部分手机有预装,应华为要求,适配 Android Q(Android 10) 版本,因为华为那边要求,新版本系统出来不久就会适配,项目是一步步适配上来的,Android M、Android N、Android O、Android P ,所以本次适配是从 Android P (9.0) 升到 Android Q,所以适配难度不是很大。建议新版本出来稳定后还是及时适配,否则一下跳跃升级适配上会比较麻烦。下面是我们项目适配遇到的问题,后面遇到问题再继续补充:
主要升级targetSdkVersion到29就可以了,我将编辑版本升到29了,support库用的是28.0.0,怕第三方库不支持没升androidx。#### Android Q (10.0)(API 29) 适配
因为项目在华为部分手机有预装,应华为要求,适配 Android Q(Android 10) 版本,因为华为那边要求,新版本系统出来不久就会适配,项目是一步步适配上来的,Android M、Android N、Android O、Android P ,所以本次适配是从 Android P (9.0) 升到 Android Q,所以适配难度不是很大。建议新版本出来稳定后还是及时适配,否则一下跳跃升级适配上会比较麻烦。下面是我们项目适配遇到的问题,后面遇到问题再继续补充:
主要升级targetSdkVersion到29就可以了,我将编辑版本升到29了,support库用的是28.0.0,怕第三方库不支持没升androidx。
targetSdkVersion : 29,
compileSdkVersion: 29,
buildToolsVersion: "29.0.2",
Android Q 之前有如下代码,获取设备Id,IMEI等
TelephonyManager telManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
telManager.getDeviceId();
telManager.getImei();
添加下面权限,并且需要动态申请权限
在 Android Q 上调用上面方法会报错
java.lang.SecurityException: getDeviceId: The user 10143 does not meet the requirements to access device identifiers.
在 Android Q 上上面方法已经不能使用了,如果获取设备唯一Id,需要使用其他方式了,谷歌提供的获取唯一标识符做法见 文档,也可以用Android_ID,上面这些也不是绝对能得到一个永远不变的Id,可能需要多种方案获取其他Id,比如有谷歌商店的手机可以使用谷歌提供的广告Id,还有其他厂商一般都会提供手机的一个唯一Id,我们项目现在使用下面这种方式 参考链接,后面会多测试一下。
public static String getUniqueID(Context context) {
String id = null;
final String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (!TextUtils.isEmpty(androidId) && !"9774d56d682e549c".equals(androidId)) {
try {
UUID uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
id = uuid.toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
if (TextUtils.isEmpty(id)) {
id = getUUID();
}
return TextUtils.isEmpty(id) ? UUID.randomUUID().toString() : id;
}
private static String getUUID() {
String serial = null;
String m_szDevIDShort = "35" +
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
Build.USER.length() % 10; //13 位
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
serial = android.os.Build.getSerial(); // TODO crash in Q
} else {
serial = Build.SERIAL;
}
//API>=9 使用serial号
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
serial = "serial"; // 随便一个初始化
}
//使用硬件信息拼凑出来的15位号码
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
在早期的测试版本新增了READ_MEDIA_IMAGES
、READ_MEDIA_AUDIO
和 READ_MEDIA_VIDEO
三个权限,正式版已经移除,还是使用之前的两个读写权限
在 Android Q 之前可以访问SD卡任意目录,使用如下:
File file = Environment.getExternalStorageDirectory();
上面得到的是SD卡根目录,打印出路径为:/storage/emulated/0。在 Android Q 上已经不能访问这个目录了,Android Q 下文件存储看下面方法。
在 App专属目录下本App可以随意操作,无需申请权限,不过 App专属目录会在App卸载时跟随删除。看下面几个目录(通过Application的context就可以访问)。
getFilesDir() :/data/user/0/本应用包名/files
getCacheDir():/data/user/0/本应用包名/cache
getExternalFilesDir(null):/storage/emulated/0/Android/data/本应用包名/files
getExternalCacheDir():/storage/emulated/0/Android/data/本应用包名/cache
getFilesDir和getCacheDir是在手机自带的一块存储区域(internal storage),通常比较小,SD卡取出也不会影响到,App的sqlite数据库和SharedPreferences都存储在这里。所以这里应该存放特别私密重要的东西。
getExternalFilesDir和getExternalCacheDir是在SD卡下(external storage),在sdcard/Android/data/包名/files和sdcard/Android/data/包名/cache下,会跟随App卸载被删除。
files和cache下的区别是,在手机设置-找到本应用-在存储中,点击清除缓存,cache下的文件会被删除,files下的文件不会。
谷歌推荐使用getExternalFilesDir。我们项目的下载是个本地功能,下载完成后是存本地数据库的,不是放网络上的,所以下载的音视频都放到了这下面,项目卸载时跟随App都删除了。getExternalFilesDir方法需要传入一个参数,传入null时得到就是sdcard/Android/data/包名/files,传入其他字符串比如"Picture"得到sdcard/Android/data/包名/files/Picture。
通过上面App专属目录只能操作本App专属目录,并且保存的文件会随着App卸载删除。通过MediaStore,App可以访问公共目录下的媒体文件,通过MediaStore操作Uri读写文件。
保存图片直接用 insertImage 方法就可以,可以传入Bitmap或图片在本地的路径,注意本地路径要是本App可以访问到的路径,否则没权读取
public void saveImage(String imagePath, String title, String desc) {
MediaStore.Images.Media.insertImage(context.getContentResolver(), imagePath, title, desc);
}
或
public void saveImage(Bitmap bitmap, String title, String desc) {
MediaStore.Images.Media.insertImage(context.getContentResolver(), bitmap, title, desc);
}
其他类型的文件保存就没有直接的方法了,大致可以用下面这样:
public void saveFile(final Uri extUri, final String mimeType, final String saveName, final String desc,
final String netUrl) {
new Thread(new Runnable() {
@Override
public void run() {
try {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, saveName);
values.put(MediaStore.Images.Media.TITLE, saveName);
values.put(MediaStore.Images.Media.DESCRIPTION, desc);
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
ContentResolver cr = context.getContentResolver();
Uri uri = cr.insert(extUri, values);
byte[] buffer = new byte[1024];
ParcelFileDescriptor parcelFileDescriptor = cr.openFileDescriptor(uri, "w");
FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor.getFileDescriptor());
URL url = new URL(netUrl);
InputStream inputStream = url.openStream();
while (true) {
int numRead = inputStream.read(buffer);
if (numRead == -1) {
break;
}
fileOutputStream.write(buffer, 0, numRead);
}
fileOutputStream.close();
parcelFileDescriptor.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
// TODO close io
}
}
}).start();
}
看上面代码,前面得到uri,然后变为fileOutputStream,后面就是文件的读写了,inputStream也可以同过其他方式得到(比如本地文件等),有输入流就可以写到uri中了。
使用如下:
// 保存图片
saveFile(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/png",
"myImage", "", "http://www.xxx.png");
// 保存视频
saveFile(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/mp4",
"myVideo", "", "http://www.xxx.mp4");
// 保存音频
saveFile(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/mpeg",
"myAudio", "", "http://www.xxx.mp3");
// Android Q 新增的下载目录
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
saveFile(MediaStore.Downloads.EXTERNAL_CONTENT_URI, "text/plain",
"myText", "", "http://www.xxx.txt");
}
文件读取,以读取图片为例,其他的也一样
获取全部图片:
public static List loadPhotoFiles(Context context) {
List photoUris = new ArrayList();
Cursor cursor = context.getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID}, null, null, null);
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor
.getColumnIndex(MediaStore.Images.Media._ID));
Uri photoUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString() + File.separator + id);
photoUris.add(photoUri);
}
return photoUris;
}
// uri 转 bitmap
public static Bitmap getBitmapFromUri(Uri uri) throws IOException {
ParcelFileDescriptor parcelFileDescriptor =
context.getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
parcelFileDescriptor.close();
return image;
}
根据title获取图片:
private Bitmap getImage(String title) {
Uri external = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
ContentResolver resolver = context.getContentResolver();
String selection = MediaStore.Images.Media.TITLE + "=?"; // 查询条件
String[] args = new String[]{title}; // 上面?的值
String[] projection = new String[]{MediaStore.Images.Media._ID}; // 查询的内容
Cursor cursor = resolver.query(external, projection, selection, args, null);
Uri imageUri = null;
if (cursor != null && cursor.moveToFirst()) {
imageUri = ContentUris.withAppendedId(external, cursor.getLong(0));
cursor.close();
}
if (imageUri == null) {
return null;
}
ParcelFileDescriptor pfd = null;
try {
pfd = getContentResolver().openFileDescriptor(imageUri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (pfd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
pfd.close();
return bitmap;
}
return null;
}
注意:MediaStore的DATA 在Android Q 之前表示文件的真实路径,在Android Q 被废弃,可以通过 _ID 获取Uri,通过 ContentUris.withAppendedId(external, cursor.getLong(0)); 获取。
删除文件,需要先查询出uri
context.getContentResolver().delete(imageUri, null, null);
修改文件,用的比较少
// 修改的内容以键值对放到ContentValues中
ContentValues values = new ContentValues();
values.put("title", "new title");
getContentResolver().update(imageUri, values, null, null);
存储访问框架(Storage Access Framework),这种方式操作文件时会拉起系统页面,通过用户授权操作来完成文件读取,用户可以选择任何目录,用户选完后App就有了这个目录的读写权限。官方文档
保存一个文件时,用下面方法
private void createFile(String fileName, String mimeType) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (!TextUtils.isEmpty(mimeType)) {
intent.setType(mimeType);
}
intent.putExtra(Intent.EXTRA_TITLE, fileName);
startActivityForResult(intent, REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = null;
if (data != null) {
uri = data.getData();
final int takeFlags = getIntent().getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);
writeFile(uri, netUrl);
}
}
}
private void writeFile(Uri uri, String netUrl) {
new Thread(new Runnable() {
@Override
public void run() {
try {
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor());
byte[] buffer = new byte[1024];
URL url = new URL(netUrl);
InputStream inputStream = url.openStream();
while (true) {
int numRead = inputStream.read(buffer);
if (numRead == -1) {
break;
}
fileOutputStream.write(buffer, 0, numRead);
}
fileOutputStream.close();
pfd.close();
inputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
调用createFile会拉起下面界面,用户保存后会回调到onActivityResult中,并将uri传来,得到uri后就可以写入了。
如下图就是通过SAF保存视频会拉起系统界面,让用户选择授权,读取删除等都差不多是这样一个界面,下面就不截图了。
读取一个文件,以读取图片为例:
public void openImage() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, READ_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = null;
if (data != null) {
uri = data.getData();
showImage(uri);
}
}
}
private void showImage(Uri uri) {
if (uri == null)
return;
ParcelFileDescriptor pfd = null;
try {
pfd = getContentResolver().openFileDescriptor(uri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (pfd != null) {
Bitmap bitmap = BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
pdf.close();
ImageView imageView = findViewById(R.id.imageView);
imageView.setImageBitmap(bitmap);
}
}
删除文件,跟上面读取一样,在onActivityResult中调用deleteImage,代码如下:
private void deleteImage(Uri uri) {
final int takeFlags = getIntent().getFlags()
& (Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);
try {
DocumentsContract.deleteDocument(getContentResolver(), uri);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
修改文件,应该也是通过Intent.ACTION_OPEN_DOCUMENT打开选择一个文件,最后在onActivityResult中得到选择文件的uri,再修改,没有具体使用过。
保存图片后需要更新到相册,之前下载图片到App 专属目录,然后通过方法1同步到相册,让用户在相册能看到下载的图片。在 Android Q 上面使用方法1不生效,应该是相册访问不到App专属目录,现在做法是通过方法2将图片存到公共目录。
// 方法1
Uri uri = Uri.fromFile(file);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
// 方法2
// 可以看上面文件存储,也可以传入Bitmap
MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), name, "");
上传:如果是App内部使用,则可以选择上传 App 专属目录下的文件,使用不需要修改;如果是想选择任意目录的文件,可以使用 SAF 的方式;如果选择系统公共目录下的文件可以使用 MediaStore 方式。在Android Q上上传非App 专属目录下的文件,上传时通过File的方式上传是不可以的,我们项目使用的是 MediaStore 方式,通过 MediaStore 得一个 Uri,然后转为 InputStream 上传,方法见下面getInputStream,大部分上传文件库应该都支持传入一个 InputStream,因为最终上传也是要获取到一个 InputStream。如果非要通过File上传文件或者需要对File做一些特殊的操作的话,最简单的方案可以将公共目录下的文件拷贝到 App 专属目录下就可以随意操作了,方法见下面 getFile。
下载:如果是App内部使用,则可以选择下载到 App 专属目录下,使用不需要修改;如果是想下载到任意目录,可以使用 SAF 的方式;如果App卸载后文件不跟随删除可以使用 MediaStore 方式。我们项目大部分都是下载到App 专属目录下了,一下卸载App后保留的文件,是通过MediaStore下载到公共目录了。
public static InputStream getInputStream(android.net.Uri uri) {
InputStream inputStream = null;
try {
ContentResolver cr = context.getContentResolver();
inputStream = cr.openInputStream(uri);
} catch (Exception e) {
e.printStackTrace();
}
return inputStream;
}
/**
* 拷贝文件,将uri拷贝到 App专属目录下
* @param uri 要拷贝文件的Uri
* @param saveName 保存到专属目录下的文件名
* @return 拷贝后新的文件
*/
public static File getFile(Uri uri, String saveName) {
File rootFile = context.getExternalFilesDir(null);
File file = new File(rootFile, saveName);
try {
byte[] buffer = new byte[1024];
FileOutputStream fileOutputStream = new FileOutputStream(file);
InputStream inputStream = context.getContentResolver().openInputStream(uri);
while (true) {
int numRead = inputStream.read(buffer);
if (numRead == -1) {
break;
}
fileOutputStream.write(buffer, 0, numRead);
}
fileOutputStream.close();
inputStream.close();
} catch (IOException e) {
file = null;
e.printStackTrace();
}
return file;
}
参考链接:Android Q 要来了,给你一份很"全面"的适配指南!