最近有个需求,助手的google卫星地图和OpenCycleMap下载的离线地图数据,要能够在内置存储和外置存储空间之间切换,因为离线瓦片数据非常大,很多户外用户希望将这些文件存储在外置TF卡上,不占用内置存储空间,所以把最近研究的整理了下,分享给大家。
需要考虑和遇到的问题(主要是不同手机、不同系统的兼容性):
1.这样获取手机所有挂载的存储器?
Android是没有提供显式的接口的,首先肯定是要阅读系统设置应用“存储”部分的源码,看存储那里是通过什么方式获取的。最后找到StorageManager和StorageVolume这2个重要的类,然后通过反射获取StorageVolume[]列表。
2.用什么标示一个存储器的唯一性?
存储路径?不行(有些手机不插TF卡,内置存储路径是/storage/sdcard0,插上TF卡后,内置存储路径变成/storage/sdcard1,TF卡变成/storage/sdcard0)。
存储卡名称?不行(可能会切换系统语言,导致名称匹配失败,名称的resId也不行,较低的系统版本StorageVolume没有mDescriptionId这一属性)。
经过测试,发现使用mStorageId可以标示存储器的唯一性,存储器数量改变,每个存储器的id不会改变。
3.如何获得存储器的名称?
经测试,不同的手机主要有3种获取存储器名换的方法:getDescription()、getDescription(Context context)、先获得getDescriptionId()再通过resId获取名称。
4.任务文件下载一半时,切换文件保存存储器,怎么处理?
有2种方案:
4.1 切换时,如果新的存储空间足够所有文件转移,先停止所有下载任务,将所有下载完和下载中的文件拷贝到新的存储空间,然后再更新下载数据库下载任务的存储路径,再恢复下载任务;
4.2 切换时,先拷贝所有下载完成的文件到新的存储空间,下载任务继续下载,下载完成再移动到新的存储空间。
5.在4.4系统上,第三方应用无法读取外置存储卡的问题。(参考“External Storage”)
google为了在程序卸载时,能够完全彻底的将程序所有数据清理干净,应用将不能向2级存储区域写入文件。
“The WRITE_EXTERNAL_STORAGE
permission must only grant write access to the primary external storage on a device. Apps must not be allowed to write to secondary external storage devices, except in their package-specific directories as allowed by synthesized permissions. Restricting writes in this way ensures the system can clean up files when applications are uninstalled.”
要能够在4.4系统上TF卡写入文件,必须先root,具体方法可以google。
所以4.4系统上,切换会导致文件转移和下载失败,用户如果要切换到TF卡,至少需要提醒用户,并最好给出4.4上root解决方法。
以下是获取存储器的部分代码:
1 public static class MyStorageVolume{ 2 public int mStorageId; 3 public String mPath; 4 public String mDescription; 5 public boolean mPrimary; 6 public boolean mRemovable; 7 public boolean mEmulated; 8 public int mMtpReserveSpace; 9 public boolean mAllowMassStorage; 10 public long mMaxFileSize; //最大文件大小。(0表示无限制) 11 public String mState; //返回null 12 13 public MyStorageVolume(Context context, Object reflectItem){ 14 try { 15 Method fmStorageId = reflectItem.getClass().getDeclaredMethod("getStorageId"); 16 fmStorageId.setAccessible(true); 17 mStorageId = (Integer) fmStorageId.invoke(reflectItem); 18 } catch (Exception e) { 19 } 20 21 try { 22 Method fmPath = reflectItem.getClass().getDeclaredMethod("getPath"); 23 fmPath.setAccessible(true); 24 mPath = (String) fmPath.invoke(reflectItem); 25 } catch (Exception e) { 26 } 27 28 try { 29 Method fmDescriptionId = reflectItem.getClass().getDeclaredMethod("getDescription"); 30 fmDescriptionId.setAccessible(true); 31 mDescription = (String) fmDescriptionId.invoke(reflectItem); 32 } catch (Exception e) { 33 } 34 if(mDescription == null || TextUtils.isEmpty(mDescription)){ 35 try { 36 Method fmDescriptionId = reflectItem.getClass().getDeclaredMethod("getDescription"); 37 fmDescriptionId.setAccessible(true); 38 mDescription = (String) fmDescriptionId.invoke(reflectItem, context); 39 } catch (Exception e) { 40 } 41 } 42 if(mDescription == null || TextUtils.isEmpty(mDescription)){ 43 try { 44 Method fmDescriptionId = reflectItem.getClass().getDeclaredMethod("getDescriptionId"); 45 fmDescriptionId.setAccessible(true); 46 int mDescriptionId = (Integer) fmDescriptionId.invoke(reflectItem); 47 if(mDescriptionId != 0){ 48 mDescription = context.getResources().getString(mDescriptionId); 49 } 50 } catch (Exception e) { 51 } 52 } 53 54 try { 55 Method fmPrimary = reflectItem.getClass().getDeclaredMethod("isPrimary"); 56 fmPrimary.setAccessible(true); 57 mPrimary = (Boolean) fmPrimary.invoke(reflectItem); 58 } catch (Exception e) { 59 } 60 61 try { 62 Method fisRemovable = reflectItem.getClass().getDeclaredMethod("isRemovable"); 63 fisRemovable.setAccessible(true); 64 mRemovable = (Boolean) fisRemovable.invoke(reflectItem); 65 } catch (Exception e) { 66 } 67 68 try { 69 Method fisEmulated = reflectItem.getClass().getDeclaredMethod("isEmulated"); 70 fisEmulated.setAccessible(true); 71 mEmulated = (Boolean) fisEmulated.invoke(reflectItem); 72 } catch (Exception e) { 73 } 74 75 try { 76 Method fmMtpReserveSpace = reflectItem.getClass().getDeclaredMethod("getMtpReserveSpace"); 77 fmMtpReserveSpace.setAccessible(true); 78 mMtpReserveSpace = (Integer) fmMtpReserveSpace.invoke(reflectItem); 79 } catch (Exception e) { 80 } 81 82 try { 83 Method fAllowMassStorage = reflectItem.getClass().getDeclaredMethod("allowMassStorage"); 84 fAllowMassStorage.setAccessible(true); 85 mAllowMassStorage = (Boolean) fAllowMassStorage.invoke(reflectItem); 86 } catch (Exception e) { 87 } 88 89 try { 90 Method fMaxFileSize = reflectItem.getClass().getDeclaredMethod("getMaxFileSize"); 91 fMaxFileSize.setAccessible(true); 92 mMaxFileSize = (Long) fMaxFileSize.invoke(reflectItem); 93 } catch (Exception e) { 94 } 95 96 try { 97 Method fState = reflectItem.getClass().getDeclaredMethod("getState"); 98 fState.setAccessible(true); 99 mState = (String) fState.invoke(reflectItem); 100 } catch (Exception e) { 101 } 102 } 103 104 /** 105 * 获取Volume挂载状态, 例如Environment.MEDIA_MOUNTED 106 */ 107 public String getVolumeState(Context context){ 108 return StorageVolumeUtil.getVolumeState(context, mPath); 109 } 110 111 public boolean isMounted(Context context){ 112 return getVolumeState(context).equals(Environment.MEDIA_MOUNTED); 113 } 114 115 public String getDescription(){ 116 return mDescription; 117 } 118 119 /** 120 * 获取存储设备的唯一标识 121 */ 122 public String getUniqueFlag(){ 123 return "" + mStorageId; 124 } 125 126 /*public boolean isUsbStorage(){ 127 return mDescriptionId == android.R.string.storage_usb; 128 }*/ 129 130 /** 131 * 获取目录可用空间大小 132 */ 133 public long getAvailableSize(){ 134 return StorageVolumeUtil.getAvailableSize(mPath); 135 } 136 137 /** 138 * 获取目录总存储空间 139 */ 140 public long getTotalSize(){ 141 return StorageVolumeUtil.getTotalSize(mPath); 142 } 143 144 @Override 145 public String toString() { 146 return "MyStorageVolume{" + 147 "\nmStorageId=" + mStorageId + 148 "\n, mPath='" + mPath + '\'' + 149 "\n, mDescription=" + mDescription + 150 "\n, mPrimary=" + mPrimary + 151 "\n, mRemovable=" + mRemovable + 152 "\n, mEmulated=" + mEmulated + 153 "\n, mMtpReserveSpace=" + mMtpReserveSpace + 154 "\n, mAllowMassStorage=" + mAllowMassStorage + 155 "\n, mMaxFileSize=" + mMaxFileSize + 156 "\n, mState='" + mState + '\'' + 157 '}' + "\n"; 158 } 159 }
1 public static List<MyStorageVolume> getVolumeList(Context context){ 2 List<MyStorageVolume> svList = new ArrayList<MyStorageVolume>(3); 3 StorageManager mStorageManager = (StorageManager)context 4 .getSystemService(Activity.STORAGE_SERVICE); 5 try { 6 Method mMethodGetPaths = mStorageManager.getClass().getMethod("getVolumeList"); 7 Object[] list = (Object[]) mMethodGetPaths.invoke(mStorageManager); 8 for(Object item : list){ 9 svList.add(new MyStorageVolume(context, item)); 10 } 11 } catch (Exception e) { 12 e.printStackTrace(); 13 } 14 return svList; 15 }
github上的测试例子:
https://github.com/John-Chen/BlogSamples/tree/master/StorageTest
如果还有什么地方没有考虑到的,欢迎讨论。