Android设置铃声分析
测试机:小米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类