測試機:小米2.3.5版本
代碼其實沒有幾行,這里簡單記錄下學習的過程.
Android系統啟動時會掃描系統與SD卡中的對媒體文件,分別存入數據庫sqlite中,以contentProvider的形式對外提供服務
路徑:/data/data/com.android.providers.media/databases/XXX...
可以看到有2個db文件, 一個是系統的,一個是sd卡里的
用SQLite Expert打開internal.db,部分截圖如下:
這里面記錄了音頻audio、視頻video、圖片images的相關數據信息,我們以音頻audio為例,藍色部分audio_meta就是audio數據表,打開之后就可以看到詳細信息了,里面列出了系統內部的所有音頻文件,各個字段在android.provider.MediaStore中都定義有相應的常量,如id --- MediaStore.Audio.Media._ID.
而這里面有想說下這四個字段
含義在源碼里都有說明,看了一遍數據,發現這四個字段同時有且僅有一個字段為1,也就是對於一個多媒體文件只能是這四種中的一種,默認為0,如果是某種類型,則android系統默認置為1,所以也就明白了為什么很多掃描系統通知或者來電鈴聲的示例代碼中,都會有一個類似的條件語句:is_notification = 1.
如:
/** * 掃描系統內部通知鈴聲 */ private void scannerMediaFile() { ContentResolver cr = this.getContentResolver(); Cursor cursor = cr.query(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, new String[] { MediaStore.Audio.Media._ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.TITLE }, "is_notification != ?", new String[] { "0" }, "_id asc"); if (cursor == null) { return; } while (cursor.moveToNext()) { data.add(cursor.getString(1)); } }
這里 is_notification != 0,效果是一樣的,除非哪天google再定義個2, 3 ......
上面扯了些其他的,關於設置鈴聲的方法,系統提供了一個鈴聲管理器android.provider.RingtoneManager,其中提供了獲取與設置鈴聲的API
如:Uri uri = RingtoneManager.getActualDefaultRingtoneUri(MediaActivity.this, RingtoneManager.TYPE_NOTIFICATION);可以獲取到當前系統的通知鈴聲uri
第二個參數可以指定獲取的鈴聲類型,還有其他的TYPE_RINGTONE,TYPE_ALARM, TYPE_ALL
設置鈴聲的API:
RingtoneManager.setActualDefaultRingtoneUri(MediaActivity.this, RingtoneManager.TYPE_NOTIFICATION, Uri.parse(data.get(position)));
第二個參數同上,最后一個是指定一個新的Uri, 這里的data.get(position)就是在上面的掃描代碼掃描出的所有通知鈴聲path路徑中選澤一個,然后在解析成一個URI對象傳入即可
那么android是如何獲取指定類型的系統鈴聲呢?
這涉及到另一個類android.provider.Settings
相關源碼如下:
public static Uri getActualDefaultRingtoneUri(Context context, int type) { //根據指定的類型獲取Settings類中對應的類型,這里RingtoneManager.TYPE_NOTIFICATION對應的為Settings.System.NOTIFICATION_SOUND,其實也就是下面所說的system表中的一個name字段名 String setting = getSettingForType(type); if (setting == null) return null; //調用Settings類中靜態內部類System中的相應方法 final String uriString = Settings.System.getString(context.getContentResolver(), setting); return uriString != null ? Uri.parse(uriString) : null; }
public synchronized static String getString(ContentResolver resolver, String name) { //MOVED_TO_SECURE是System類中定義的一個hashSet集合,在Android系統啟動時,會初始化30(目前是30)條涉及系統安全的設置數據(如果http代理設置,wifi相關設置),並且存入數據庫中,與多媒體的db不同,系統默認存放在settings.db中,路徑為/data/data/com.android.providers.settings/databases,具體是存放在settings.db數據庫實例的secure表中,用工具打開,可以看到此表中恰好有30條數據。說了那么多,其實這里是檢查你所指定的類型也就是db中的字段在不在這個集合中,如果在,則會調用Settings類中的另一個靜態內部類Secure中的getString(...)方法 if (MOVED_TO_SECURE.contains(name)) { Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + " to android.provider.Settings.Secure, returning read-only value."); return Secure.getString(resolver, name); } //如果不在那個涉及系統安全的設置集合中,則調用Settings中定義的一個緩存類NameValueCache中的getString(...) if (sNameValueCache == null) { sNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI, CALL_METHOD_GET_SYSTEM); } return sNameValueCache.getString(resolver, name); }
//NameValueCache中getString()方法部分代碼 Cursor c = null; try { //mUri == "content://settings/system"在NameValueCache初始化時賦值,指定查詢的是settings.db中的system表,同理上面提到的Secure類的getString(...)中調用的也是這個緩存類的同名方法,只不過mUri被指定為查詢secure表(這2個表中除了id,只有name與value2個字段,分別指定設置的類型與對應的值) c = cp.query(mUri, SELECT_VALUE, NAME_EQ_PLACEHOLDER, new String[]{name}, null); if (c == null) { Log.w(TAG, "Can't get key " + name + " from " + mUri); return null; } String value = c.moveToNext() ? c.getString(0) : null; synchronized (this) { //查詢完講name/value鍵值對放入mValues集合中,當然如果這個集合中已經存在這個鍵值對,那么也就不會執行這段操作db的代碼了 mValues.put(name, value); }
settings.db結構如下:
上面示例中指定的TYPE_NOTIFICATION的數據如下(藍色部分):
最后返回的就是file:///..........這個String數據,再轉化成URI返回給調用者
OK,那么設置鈴聲的API, setAc.......執行的過程也類似:
public static boolean putString(ContentResolver resolver, String name, String value) { //這里依然是檢查設置的類型是否涉及到系統預置的安全設置集合,如果是,則直接返回false if (MOVED_TO_SECURE.contains(name)) { Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.System" + " to android.provider.Settings.Secure, value is unchanged."); return false; } //這里執行的是另一個靜態內部類NameValueTable中的方法 return putString(resolver, CONTENT_URI, name, value); }
protected static boolean putString(ContentResolver resolver, Uri uri, String name, String value) { // The database will take care of replacing duplicates. try { ContentValues values = new ContentValues(); values.put(NAME, name); values.put(VALUE, value); //指定類型name與相應value插入db的system表中,如果表中已經存在指定的類型字段怎么辦? 請看上面的源碼注釋... resolver.insert(uri, values); return true; } catch (SQLException e) { Log.w(TAG, "Can't set key " + name + " in " + uri, e); return false; } }
最后總結下,從整個過程可以看到android系統的一些設計思想
1,設置鈴聲之前,要先知道有哪些系統鈴聲,所以需要掃描,android提供了xxx.media這個contentProvider為此服務,對應的數據庫為internal.db/external-xx.db
2,拿到鈴聲,真正需要設置的時候,提供了Setting類管理這個過程,其對應的數據庫為settings.db
2.1 首先檢查是否涉及到系統的一些安全設置參數,這里定義了Secure類來管理,如果涉及到系統安全,那么又分為兩種情況:
2.1.1 如果是查詢,則操作secure 表查詢
2.1.2 如果是寫操作,則直接return
2.2 不涉及到系統安全,就屬於正常設置,接着定義了System類管理
3,查詢操作的實際操作類NameValueCache, 其中定義了
緩存name/value鍵值對的集合,避免每次操作都去操作數據庫
可以由調用者指定的uri,便於根據uri決定去操作哪張表
以及寫操作的NameValueTable類,因為寫操作涉及到id, 所以繼承了BaseColumns類