升级Android Studio及minSdk至31后,获取图片Exif经纬度失效
我之前用这段代码获取图片的GPS地址一直正常:
private static AddressVO getAddress(ExifInterface exifInterface, Context context){ if (exifInterface == null){ return null; } String latValue = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE); String lonValue = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); String latRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); String lonRef = exifInterface.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); return AddressUtils.getAddress(context, latValue, latRef, lonValue, lonRef); }
现在这段代码失效了,获取到的latValue和lonValue是"0/1,0/1,0/1",latRef和lonRef是空字符串,导致没法通过经纬度解析地理位置。
问题出在我把Android Studio升级到Flamingo版本,同时把minSdkVersion从28升到31之后,升级后出现了一堆兼容性问题,这个问题也是其中之一,我还没找到解决办法。
我确定不是权限问题,因为确实能读到值,只是值不对;用的照片都是之前能正常工作的有效照片,现在也失效了。
请问该怎么修复?
更新1:获取ExifInterface的代码
用户从相册上传图片,我的处理逻辑如下:
private static List<ConsumeRecordVO> buildRecords(Context context, ClipData clipData){ List<ConsumeRecordVO> records = new ArrayList<>(); int count = clipData.getItemCount(); for (int i = 0; i < count; i++){ Uri uri = clipData.getItemAt(i).getUri(); String filename = ImageUtils.newImageFileName(i); if (!ImageUtils.saveImage(context, uri, filename)){ continue; } ConsumeRecordVO record = new ConsumeRecordVO(); RestaurantFoodVO food = new RestaurantFoodVO(); food.setCover(filename); food.setImages(Lists.newArrayList(filename)); record.setFoods(Lists.newArrayList(food)); record.setEaters(new ArrayList<>()); records.add(record); // 我测试用的照片路径是/storage/emulated/0/DCIM/Camera/IMG_20230721_115842.jpg,在我手机上是正确的 String path = FileUtils.getPath(context, uri); ExifInterface exifInterface; try { exifInterface = new ExifInterface(path); } catch (Exception e) { Log.w("buildRestaurantFromImages", "ExifInterface", e); exifInterface = null; } record.setConsumeTime(getConsumeTime(exifInterface, path)); record.setAddress(getAddress(exifInterface, context)); } return records; }
更新2:Uri转路径的工具类及打开相册代码
FileUtils(Uri转物理路径)
/** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and * other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @author paulburke */ public static String getPath(final Context context, final Uri uri) { // DocumentProvider if (DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } // TODO handle non-primary volumes } // DownloadsProvider else if (isDownloadsDocument(uri)) { final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.parseLong(id)); return getDataColumn(context, contentUri, null, null); } // MediaProvider else if (isMediaDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[] { split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } // MediaStore (and general) else if ("content".equalsIgnoreCase(uri.getScheme())) { return getDataColumn(context, uri, null, null); } // File else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } return null; } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int column_index = cursor.getColumnIndexOrThrow(column); return cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); }
打开相册的代码
public static void openGallery(Activity activity, View caller){ GALLERY_CALLER = caller == null ? 0 : caller.getId(); if (!PermissionUtils.checkAndRequestReadStoragePermission(activity)){ Toast.makeText(activity, "no perm", Toast.LENGTH_SHORT).show(); return; } Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); activity.startActivityForResult(Intent.createChooser(intent, activity.getString(R.string.select_image)), ActivityCodeConstant.GALLERY); }
问题原因与修复方案
核心问题
从Android 11(API 30)开始,Scoped Storage限制了应用直接通过物理路径访问媒体文件的权限,即使拿到正确路径,ExifInterface通过路径打开文件时也可能无法读取完整的Exif元数据(如GPS信息),只能得到默认无效值。另外,你先调用ImageUtils.saveImage复制图片的操作,若未保留Exif元数据,后续读取的就是丢失了GPS信息的复制文件,也会导致问题。
修复步骤
改用ContentResolver输入流初始化ExifInterface
绕过Scoped Storage限制,直接通过Uri打开输入流读取原始图片的Exif数据:// 替换原通过path创建ExifInterface的代码 ExifInterface exifInterface = null; try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { if (inputStream != null) { exifInterface = new ExifInterface(inputStream); } } catch (Exception e) { Log.w("buildRestaurantFromImages", "ExifInterface", e); }确保图片保存时保留Exif信息
若ImageUtils.saveImage使用BitmapFactory和Bitmap.compress()方式保存,会丢失Exif数据,需改为直接复制文件:// 示例:直接复制文件保留完整Exif public static boolean saveImage(Context context, Uri uri, String filename) { try (InputStream inputStream = context.getContentResolver().openInputStream(uri); OutputStream outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE)) { byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) > 0) { outputStream.write(buffer, 0, length); } return true; } catch (IOException e) { e.printStackTrace(); return false; } }移除过时的路径转换逻辑
FileUtils.getPath依赖的_data字段在Android 10+已过时,且部分设备无法返回正确路径,建议完全移除该逻辑,所有媒体操作都通过Uri和ContentResolver完成,不再依赖物理路径。适配权限规则
- Android 13(API 33)及以上,需申请
READ_MEDIA_IMAGES权限,而非READ_EXTERNAL_STORAGE - Android 10+即使没有
READ_EXTERNAL_STORAGE权限,通过MediaStore和ContentResolver仍可访问媒体文件,确保权限请求逻辑正确
- Android 13(API 33)及以上,需申请
验证修复
修改后测试原本能正常读取GPS的照片,确认latValue、lonValue、latRef、lonRef能获取到原始有效数据,再通过AddressUtils解析即可恢复正常。
内容的提问来源于stack exchange,提问作者Seaky Lone

