Android(安卓)Q 适配
因为项目在华为部分手机有预装,应华为要求,适配 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",
应用读取 Device ID
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专属目录会在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。
使用MediaStore访问公共目录
通过上面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);
使用SAF访问指定目录
存储访问框架(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,再修改,没有具体使用过。
下面是我们项目在文件存储中遇到的问题
1、更新图片到图库
保存图片后需要更新到相册,之前下载图片到App 专属目录,然后通过方法1同步到相册,让用户在相册能看到下载的图片。在 Android Q 上面使用方法1不生效,应该是相册访问不到App专属目录,现在做法是通过方法2将图片存到公共目录。
// 方法1Uri uri = Uri.fromFile(file);sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));// 方法2// 可以看上面文件存储,也可以传入BitmapMediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), name, "");
2、文件上传下载
上传:如果是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 要来了,给你一份很"全面"的适配指南!
更多相关文章
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- Android(安卓)studio+SQLCipher加密SQLite数据库的几个坑
- android NDK 和android,mk文件 认知
- Gradle使用详解
- [置顶] android怎样调用@hide和internal API
- UBUNTU下以MTP模式自动挂载NEXUS 7