前言
当我在使用音乐播放器和各种小说APP的过程中,感觉非常不好的一个体验就是你需要通过手动点击全盘检索后,新下载的数据、从磁盘拷贝的数据才会更新显示在列表上,这对于我们来说看上去没有什么不对,但从用户的角度出发这是一个非常不好的体验,因为多数人是根本不知道全盘检索这个概念的,手动更新APP的本地数据无形之中增加了用户使用APP的学习成本。
我们之前做的小系统APP也是通过手动检索这种方式来刷新程序中的本地数据列表的,在接触到Android的媒体库后,发现这个问题能够通过观察者模式监听Android媒体数据库变化的方式来实现APP本地数据的自动更新,在成功实现这个功能后,现在将其总结下来方便后面查看。
Android媒体库
· 媒体库是什么?:在Android系统中,为了提高应用检索数据的效率,Android会将存储在文件系统中的文件信息保存在一个数据库文件中,这样在应用中就可以通过读取该数据库来快速查找满足APP需求的文件列表,比如一个电子书阅读APP,通过如下方法就可以获取到媒体库中存在的电子书文件列表,保存哪些格式的文件是可以通过修改Android原来来调整的,不过对于多媒体文件来说,Android原生系统默认就保存在媒体库中了:
[代码]java代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | /** * 从媒体库中获取指定后缀的文件列表 * * @param searchFileSuffix 文件后缀列表,eg: new String[]{"epub","mobi","pdf","txt"}; * @return 指定后缀的文件列表 * */ public static ArrayList<string> getSupportFileList(Context context, String[] searchFileSuffix) { ArrayList<string deep="9"> searchFileList = null; if (null == context || null == searchFileSuffix || searchFileSuffix.length == 0) { return null; }
String searchPath = ""; int length = searchFileSuffix.length; for (int index = 0; index < length; index++) { searchPath += (MediaStore.Files.FileColumns.DATA + " LIKE '%" + searchFileSuffix[index] + "' "); if ((index + 1) < length) { searchPath += "or "; } } searchFileList = new ArrayList<string>(); Uri uri = MediaStore.Files.getContentUri("external"); Cursor cursor = context.getContentResolver().query( uri,new String[] { MediaStore.Files.FileColumns.DATA,MediaStore.Files.FileColumns.SIZE }, searchPath, null,null);
if (cursor == null) { System.out.println("Cursor 获取失败!"); } else { if (cursor.moveToFirst()) { do { String filepath = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)); if (isFileExist(filepath)) { try { searchFileList.add(new String(filepath.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } }
} while (cursor.moveToNext()); }
if (!cursor.isClosed()) { cursor.close(); } }
return searchFileList; }
/** * 判断SD卡上的文件夹是否存在 * * @param fileName 文件名 * @return true 文件存在,false 文件不存在 */ private static boolean isFileExist(String filePath) { File file = null; boolean isExist = false;
if (null != filePath) { file = new File(filePath); isExist = (null != file && file.isFile()) ? file.exists() : false; if (isExist && null != file && 0 == file.length()) { isExist = false; } }
return isExist; }</string></string></string> |
· 媒体库更新时机:Android系统会在系统开机、USB插拔、TF卡插拔的时候自动更新媒体库(将新增的文件添加到媒体库中,移除不存在的文件数据记录),除了Android系统会自动更新媒体库文件外,开发者也可以在程序中手动更新媒体库,这样能够在文件系统中有新的文件或者通过程序删掉某些文件时能够将动态及时更新到媒体库,保证媒体库中的文件信息是实时的,更新的具体方式如下:
context.sendBroadcast( new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse( “file://” + filePath ) ) );
注:上面介绍是的更新单个文件的方式,Android没有提供直接更新整个文件夹的方式,如果是整个文件夹,可以先得到文件夹下的所有文件路径列表,然后挨个更新,对于这个如果有更先进的方法欢迎提出。
APP本地数据自动更新的具体实现
类似于上述的音视频播放器、小说阅读APP,如果我们需要实现本地数据APP自动更新的功能,只要保持APP支持文件列表的数据库和媒体库中的对应格式的文件同步就可以了,所以我们需要做的是:监听媒体库中文件列表的变化,然后将变化告知APP即可,原理是:
· 通过广播监听USB插拔、TF卡插拔,如果检查到在APP运行过程中有这些操作,直接通过APP全盘检索;
· 通过观察者模式监听媒体库中的文件变化,如果有变化,每隔五秒钟将APP现存列表和媒体库中检索到对应格式的文件列表做比较,如果列表有变化,则将变化的列表更新给APP;
· 在APP进入、退出时注册/反注册广播、打开/关闭计时器。
整个代码非常简单,一个类搞定,具体代码如下:
[代码]java代码:
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Timer;
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.provider.MediaStore;
/** * 自动更新书架 * * */ public class AutoRefreshBookShelf { public AutoRefreshBookShelf( Context context, AutoRefreshListener autoRefreshListener, String[] supportSuffix ) throws NullPointerException{ if( null == context || null == autoRefreshListener || null == supportSuffix ){ throw new NullPointerException( "传非空的参数进来!" ); }
mContext = context; mAutoRefreshListener = autoRefreshListener; mSupportSuffix = supportSuffix;
initAutoRefreshBookShelf( ); }
// 不在本界面停止后台检索 public void onPause( ){ stopCheckFileTimer( ); }
// 返回界面恢复后台检索 public void onResume( ){ startCheckFileTimer( ); }
/** * 注销广播 * * */ public void unregisterAutoRefreshBookShelf( ) throws NullPointerException{ if( null == mBroadcastReceiver || null == mMediaStoreChangeObserver || null == mContext ){ throw new NullPointerException( "没有初始化" ); } mContext.unregisterReceiver( mBroadcastReceiver ); mContext.getContentResolver( ).unregisterContentObserver( mMediaStoreChangeObserver ); stopCheckFileTimer( ); }
/** * 得到变化的文件列表 * * */ public void getChangedFileList( ){ System.out.println( "toast ================= getChangedFileList " ); startCheckFileTimer( ); }
private void initAutoRefreshBookShelf( ){ startMediaFileListener( ); observerMediaStoreChange( ); }
private void observerMediaStoreChange( ){ if( null == mMediaStoreChangeObserver ){ mMediaStoreChangeObserver = new MediaStoreChangeObserver( ); } mContext.getContentResolver( ).registerContentObserver( MediaStore.Files.getContentUri("external"), false, mMediaStoreChangeObserver ); }
/** * 监听USB的状态,更新书架书本信息 * * */ private void startMediaFileListener( ){ if( null != mBroadcastReceiver ){ return; }
IntentFilter intentFilter = new IntentFilter( ); intentFilter.addAction( Intent.ACTION_MEDIA_SCANNER_FINISHED ); intentFilter.addAction( Intent.ACTION_MEDIA_MOUNTED ); intentFilter.addAction( Intent.ACTION_MEDIA_EJECT ); intentFilter.addDataScheme( "file" );
mBroadcastReceiver = new BroadcastReceiver(){ @Override public void onReceive(Context context,Intent intent){ String action = intent.getAction( ); if( Intent.ACTION_MEDIA_SCANNER_FINISHED.equals( action ) ){ System.out.println( "toast ================= ACTION_MEDIA_SCANNER_FINISHED " ); mTimerWorking = false; startCheckFileTimer( ); }else if( action.equals( Intent.ACTION_MEDIA_MOUNTED ) ){ System.out.println( "toast ================= ACTION_MEDIA_MOUNTED or ACTION_MEDIA_EJECT " ); mTimerWorking = true; mAutoRefreshListener.onBookScan( ); }else if( action.equals( Intent.ACTION_MEDIA_EJECT ) ){ mAutoRefreshListener.onBookScan( ); } } }; mContext.registerReceiver( mBroadcastReceiver, intentFilter );//注册监听函数 }
/** * 媒体数据库变更观察类 * * */ class MediaStoreChangeObserver extends ContentObserver{ public MediaStoreChangeObserver( ) { super( new Handler( ) ); }
@Override public void onChange(boolean selfChange) { super.onChange(selfChange); startCheckFileTimer( ); } }
private void startCheckFileTimer( ){ if( mTimerWorking ){ return; }
mCheckFileTimer = new Timer( ); mCheckFileTimer.schedule( new CheckFileChangeTimerTask( ), 5000 ); mTimerWorking = true; }
private void stopCheckFileTimer( ){ if( null != mCheckFileTimer ){ mCheckFileTimer.cancel( ); mCheckFileTimer = null;
mTimerWorking = false; } }
/** * 得到新增的文件列表 * * */ public ArrayList<string> getChangedFileList( Context context, String[] searchFileSuffix, ArrayList<string> existFileList ){ ArrayList<string> changedFileList = null; if( null == context || null == searchFileSuffix ){ return changedFileList; }
ArrayList<string> supportFileList = getSupportFileList( context, searchFileSuffix ); changedFileList = getDifferentFileList( supportFileList, existFileList ); if( null == changedFileList || changedFileList.size( ) == 0 ){ changedFileList = null; }
return changedFileList; }
/** * 获取新增的文件列表 * * */ private ArrayList<string> getDifferentFileList( ArrayList<string> newFileList, ArrayList<string> existFileList ){ ArrayList<string> differentFileList = null; if( null == newFileList || newFileList.size( ) == 0 ){ return differentFileList; }
differentFileList = new ArrayList<string>( ); boolean isExist = false; if( null == existFileList ){ // 如果已存在文件为空,那肯定是全部加进来啦。 for( String newFilePath : newFileList ){ differentFileList.add( newFilePath ); } }else{ for( String newFilePath : newFileList ){ isExist = false; for( String existFilePath : existFileList ){ if( existFilePath.equals( newFilePath ) ){ isExist = true; break; } }
if( !isExist ){ differentFileList.add( newFilePath ); } } }
return differentFileList; }
/** * 从媒体库中获取指定后缀的文件列表 * * */ public ArrayList<string> getSupportFileList( Context context, String[] searchFileSuffix ) { ArrayList<string> searchFileList = null; if( null == context || null == searchFileSuffix || searchFileSuffix.length == 0 ){ return null; }
String searchPath = ""; int length = searchFileSuffix.length; for( int index = 0; index < length; index++ ){ searchPath += ( MediaStore.Files.FileColumns.DATA + " LIKE '%" + searchFileSuffix[ index ] + "' " ); if( ( index + 1 ) < length ){ searchPath += "or "; } }
searchFileList = new ArrayList<string>(); Uri uri = MediaStore.Files.getContentUri("external"); Cursor cursor = context.getContentResolver().query( uri, new String[] { MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns._ID }, searchPath, null, null);
String filepath = null; if (cursor == null) { System.out.println("Cursor 获取失败!"); } else { if (cursor.moveToFirst()) { do { filepath = cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)); try { searchFileList.add(new String(filepath.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e) { e.printStackTrace(); }
} while (cursor.moveToNext()); }
if (!cursor.isClosed()) { cursor.close(); } }
return searchFileList; }
/** * 得到媒体库更新的文件 * * */ class GetMediaStoreDataTask extends AsyncTask< Void , Void , Void>{ @Override protected Void doInBackground(Void... arg0) { ArrayList<string> changedFileList = getChangedFileList( mContext, mSupportSuffix, mAutoRefreshListener.onGetBookPathList( ) ); if( null != changedFileList && changedFileList.size( ) > 0 ){ mAutoRefreshListener.onBookRefresh( changedFileList ); } mTimerWorking = false;
return null; } }
class CheckFileChangeTimerTask extends java.util.TimerTask{ @Override public void run() { new GetMediaStoreDataTask( ).execute( ); } }
/** * 书架自动刷新接口 * * */ public interface AutoRefreshListener{ public ArrayList<string> onGetBookPathList( ); // 得到书架书本列表 public void onBookRefresh( ArrayList<string> bookInfoList );// 刷新书架 public void onBookScan( );//全盘扫描书架 }
private boolean mTimerWorking = false; private Context mContext = null; private String[] mSupportSuffix = null; private BroadcastReceiver mBroadcastReceiver = null; private MediaStoreChangeObserver mMediaStoreChangeObserver = null; private AutoRefreshListener mAutoRefreshListener = null; private Timer mCheckFileTimer = null; }</string></string></string></string></string></string></string></string></string></string></string></string></string></string></string> |
注意:建议该功能只在APP运行时开启,因为现实的文件列表只有你真正在使用APP时才会去查看,所以没有必要通过这种方式在后台操作。