天天看點

NotificationManager 通知聲音控制邏輯

主要分析Notification通知聲音流程

一般使用Notification用法如下:

// 建立一個NotificationManager的引用   
NotificationManager notificationManager = (NotificationManager)    
     this.getSystemService(android.content.Context.NOTIFICATION_SERVICE);   

// 定義Notification的各種屬性   
Notification notification =new Notification(R.drawable.icon,   
    "督導系統", System.currentTimeMillis()); 
//FLAG_AUTO_CANCEL   該通知能被狀态欄的清除按鈕給清除掉
//FLAG_NO_CLEAR      該通知不能被狀态欄的清除按鈕給清除掉
//FLAG_ONGOING_EVENT 通知放置在正在運作
//FLAG_INSISTENT     是否一直進行,比如音樂一直播放,知道使用者響應
notification.flags |= Notification.FLAG_ONGOING_EVENT; // 将此通知放到通知欄的"Ongoing"即"正在運作"組中   
notification.flags |= Notification.FLAG_NO_CLEAR; // 表明在點選了通知欄中的"清除通知"後,此通知不清除,經常與FLAG_ONGOING_EVENT一起使用   
notification.flags |= Notification.FLAG_SHOW_LIGHTS;   
//DEFAULT_ALL     使用所有預設值,比如聲音,震動,閃屏等等
//DEFAULT_LIGHTS  使用預設閃光提示
//DEFAULT_SOUNDS  使用預設提示聲音
//DEFAULT_VIBRATE 使用預設手機震動,
//需加上<uses-permission android:name="android.permission.VIBRATE" />權限
notification.defaults = Notification.DEFAULT_LIGHTS; 
//疊加效果常量
notification.defaults=Notification.DEFAULT_LIGHTS|Notification.DEFAULT_SOUND;
notification.ledARGB = Color.BLUE;   
notification.ledOnMS =; //閃光時間,毫秒

// 設定通知的事件消息   
CharSequence contentTitle ="督導系統标題"; // 通知欄标題   
CharSequence contentText ="督導系統内容"; // 通知欄内容   
Intent notificationIntent =new Intent(MainActivity.this, MainActivity.class); // 點選該通知後要跳轉的Activity   
PendingIntent contentItent = PendingIntent.getActivity(this, , notificationIntent, );   
notification.setLatestEventInfo(this, contentTitle, contentText, contentItent);   

// 把Notification傳遞給NotificationManager   
 notificationManager.notify(, notification);   
           

給notification設定聲音有2種方式

  • notification.defaults 一般是使用這種,使用系統預設聲音
  • notification.sound 使用自定義聲音,值為一個聲音檔案的Uri位址

當調用NotificationManager的notify方法,通知才會被觸發

public void notify(String tag, int id, Notification notification)
    {
        int[] idOut = new int[];
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        //如果設定了notification.sound屬性,會檢測該聲音檔案是否可以正常播放
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        try {
            //将通知添加棧裡面
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    notification, idOut, UserHandle.myUserId());
            if (id != idOut[]) {
                Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[]);
            }
        } catch (RemoteException e) {
        }
    }
           

代碼第16行,可以看出具體實作是調用NotificationManagerService類的enqueueNotificationWithTag方法來實作的,

// Notifications
    // ============================================================================
    public void enqueueNotificationWithTag(String pkg, String basePkg, String tag, int id,
            Notification notification, int[] idOut, int userId)
    {
        enqueueNotificationInternal(pkg, basePkg, Binder.getCallingUid(), Binder.getCallingPid(),
                tag, id, notification, idOut, userId);
    }
           

繼續調用裡面的方法

// Not exposed via Binder; for system use only (otherwise malicious apps could spoof the
    // uid/pid of another application)
    public void enqueueNotificationInternal(final String pkg, String basePkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int[] idOut, int incomingUserId){
        //...
        //...
​
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                //...
                //...
                synchronized (mNotificationList) {

                    // If we're not supposed to beep, vibrate, etc. then don't.
                    if (((mDisabledNotifications & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) == )
                            && (!(old != null
                                && (notification.flags & Notification.FLAG_ONLY_ALERT_ONCE) !=  ))
                            && (r.getUserId() == UserHandle.USER_ALL ||
                                (r.getUserId() == userId && r.getUserId() == currentUser))
                            && canInterrupt
                            && mSystemReady) {
​
                        final AudioManager audioManager = (AudioManager) mContext
                        .getSystemService(Context.AUDIO_SERVICE);
​
                        // sound
​
                        // should we use the default notification sound? (indicated either by
                        // DEFAULT_SOUND or because notification.sound is pointing at
                        // Settings.System.NOTIFICATION_SOUND)
                        final boolean useDefaultSound =
                               (notification.defaults & Notification.DEFAULT_SOUND) !=  ||
                                       Settings.System.DEFAULT_NOTIFICATION_URI
                                               .equals(notification.sound);
​
                        Uri soundUri = null;
                        boolean hasValidSound = false;
                        //先判斷聲音檔案是否是合法的Uri檔案
                        if (useDefaultSound) {
                            soundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
​
                            // check to see if the default notification sound is silent
                            ContentResolver resolver = mContext.getContentResolver();
                            hasValidSound = Settings.System.getString(resolver,
                                   Settings.System.NOTIFICATION_SOUND) != null;
                        } else if (notification.sound != null) {
                            soundUri = notification.sound;
                            hasValidSound = (soundUri != null);
                        }

                        if (hasValidSound) {
           

從第220行起,會判斷是否聲音檔案是否是合法檔案

// sound
                        // should we use the default notification sound? (indicated either by
                        // DEFAULT_SOUND or because notification.sound is pointing at
                        // Settings.System.NOTIFICATION_SOUND)
                        final boolean useDefaultSound =
                               (notification.defaults & Notification.DEFAULT_SOUND) !=  ||
                                       Settings.System.DEFAULT_NOTIFICATION_URI
                                               .equals(notification.sound);
                        Uri soundUri = null;
                        boolean hasValidSound = false;
                        //先判斷聲音檔案是否是合法的Uri檔案
                        if (useDefaultSound) {
                            soundUri = Settings.System.DEFAULT_NOTIFICATION_URI;
                            // check to see if the default notification sound is silent
                            ContentResolver resolver = mContext.getContentResolver();
                            hasValidSound = Settings.System.getString(resolver,
                                   Settings.System.NOTIFICATION_SOUND) != null;
                        } else if (notification.sound != null) {
                            soundUri = notification.sound;
                            hasValidSound = (soundUri != null);
                        }
           

從代碼裡面看出,先判斷是否使用預設的聲音檔案,如果是,則從SettingProvider裡面擷取聲音檔案uri;如果不使用預設,則通過otification.sound來擷取uri檔案。

如果notification.defaults設定為Notification.DEFAULT_SOUND,則設定notification.sound會沒有效果。是以如果想 notification.sound有效果,則在設定notification.defaults時,不能設定為Notification.DEFAULT_SOUND。

注意,這個Uri檔案不一定存在。

繼續看代碼,第240行

if (hasValidSound) {
    boolean looping = (notification.flags & Notification.FLAG_INSISTENT) != ;
    int audioStreamType;
    if (notification.audioStreamType >= ) {
       audioStreamType = notification.audioStreamType;
    } else {
       audioStreamType = DEFAULT_STREAM_TYPE;
    }
    mSoundNotification = r;
    // do not play notifications if stream volume is  (typically because
    // ringer mode is silent) or if there is a user of exclusive audio focus
    if ((audioManager.getStreamVolume(audioStreamType) != )
           && !audioManager.isAudioFocusExclusive()) {
       final long identity = Binder.clearCallingIdentity();
       //播放檔案時有一個捕獲異常的操作,如果Uri檔案不存在時,會抛出異常
       try {
           final IRingtonePlayer player = mAudioService.getRingtonePlayer();
           if (player != null) {
                  player.playAsync(soundUri, user, looping, audioStreamType);
            }
        } catch (RemoteException e) {
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }
}
           

如果想控制是有通知聲音,這一段代碼提供了思路,可以通過設定通知音量來控制通知是否有聲音。

Anddroid各聲音通道的預設值是

public static final int[] DEFAULT_STREAM_VOLUME = new int[] {
        ,  // STREAM_VOICE_CALL
        ,  // STREAM_SYSTEM
        ,  // STREAM_RING
        , // STREAM_MUSIC
        ,  // STREAM_ALARM
        ,  // STREAM_NOTIFICATION
        ,  // STREAM_BLUETOOTH_SCO
        ,  // STREAM_SYSTEM_ENFORCED
        , // STREAM_DTMF
        ,  // STREAM_TTS
        ,  // STREAM_FM
          // STREAM_MATV
    };
           

Android系統音量鍵響應邏輯

android中将音量的調節分為全局音量(MasterVolume)和個别音量(StreamVolume)。

StreamVolume在靜音後,調節音量大小會從0開始,不會從靜音之前的音量開始調節。

MasterVolume則能夠從靜音之前的音量開始。

如何設定預設使用MasterVolume,還是StreamVolume呢!

在AudioManager對象中有對靜音按鍵的處理:

public void handleKeyDown(KeyEvent event, int stream) {
        int keyCode = event.getKeyCode();
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_UP:
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                /*
                 * Adjust the volume in on key down since it is more
                 * responsive to the user.
                 */
                int flags = FLAG_SHOW_UI | FLAG_VIBRATE;

                if (mUseMasterVolume) {
                    adjustMasterVolume(
                            keyCode == KeyEvent.KEYCODE_VOLUME_UP
                                    ? ADJUST_RAISE
                                    : ADJUST_LOWER,
                            flags);
                } else {
                    adjustSuggestedStreamVolume(
                            keyCode == KeyEvent.KEYCODE_VOLUME_UP
                                    ? ADJUST_RAISE
                                    : ADJUST_LOWER,
                            stream,
                            flags);
                    mVolume = getStreamVolume(STREAM_MUSIC);
                    Log.d(TAG,"mVolume = "+mVolume);
                }
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                if (event.getRepeatCount() == ) {
                    if (mUseMasterVolume) {
                        setMasterMute(!isMasterMute());
                    } else {
                        // TODO: Actually handle MUTE.                       
                        if(getStreamVolume(STREAM_MUSIC) != )
                        {
                            mVolume = getStreamVolume(STREAM_MUSIC);
                            Log.d(TAG,"mVolume = "+mVolume);
                            setStreamVolume(STREAM_MUSIC, , FLAG_SHOW_UI);
                        }
                        else
                        {
                            Log.d(TAG,"mVolume = "+mVolume);
                            setStreamVolume(STREAM_MUSIC, mVolume, FLAG_SHOW_UI);
                        }
                    }
                }
                break;
        }
    }
           
mUseMasterVolume控制了使用streamvolume還是mastervolume。

而mUseMasterVolume是從AudioManager的構造函數中初始化的
           
mUseMasterVolume = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_useMasterVolume);
    com.android.internal.R.bool.config_useMasterVolume又是什麼呢?是framework的配置資訊。
           

可以從frameworks/base/core/res/res下的values/config.xml中找到如下一行:

<bool name="config_useMasterVolume">false</bool>
           

把false改為true即可。

Android修改Notification音量

主要涉及到2個db,internal.db和settings.db:

1.internal.db

檔案位置/data/data/com.android.providers.media/databases/internal.db。裡面儲存了系統預設通知聲音資料,聲音檔案位于system/media/audio/notifications。其中notification是在audio_meta表中的,每個檔案都對應有一個id

該db是在系統啟動時,MediaScanner将掃描到的檔案寫入

2.settings.db

檔案位于data/data/com.android.providers.settings/databases/settings.db.這個db檔案儲存了系統設定的一些預設值。

在system表中,可以看到預設通知聲音檔案:

3.Notification.DEFAULT_SOUND

當一個Notification設定預設聲音為

Notification.DEFAULT_SOUND mUsbStorageNotification.defaults |= Notification.DEFAULT_SOUND

系統會讀取預設通知聲音,使用settings.idb裡面的notification_sound值,此值是一個Uri,通過此Uri可以擷取到檔案位置

實用代碼

/**
     * get the default notification sound from database ,
     * {@link MediaStore.Audio.Media.INTERNAL_CONTENT_URI}
     * 
     * @param context
     * @return The default notification sound sound uri
     */
    private String getDefaultNotificationSound(Context context) {
        String target = DEFAULT_NOTIFICATION_SOUND_URI;
        String fileName = SystemProperties.get("ro.config.notification_sound", "");
        Log.v(TAG, "ro.config.notification_sound is " + fileName);
        Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.INTERNAL_CONTENT_URI, null, null,
                null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
        while (cursor != null && cursor.moveToNext()) {
            String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME));
            if (fileName.equalsIgnoreCase(name)) {
                target = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
                break;
            }
        }
        if (cursor != null) {
            cursor.close();
        }
        return target;
    }
           

4.使用AudioManager裡面的方法來控制音量

//調節音量加減
adjustStreamVolume(int streamType, int direction, int flags)
//設定音量
setStreamVolume(int streamType, int index, int flags)
//對單獨的streamType設定靜音
setStreamMute(int streamType, boolean state)
           

a) 其中adjustStreamVolume和setStreamVolume都和前面講到的mUseMasterVolume有關,會擷取此變量影響;

b) setStreamMute與mUseMasterVolume無關,但是與AudioService裡面的mMuteAffectedStreams變量有關

/** @see AudioManager#setStreamMute(int, boolean) */
    public void setStreamMute(int streamType, boolean state, IBinder cb) {
        if (mUseFixedVolume) {
            return;
        }
​
        if (isStreamAffectedByMute(streamType)) {
            mStreamStates[streamType].mute(cb, state);
        }
    }
           

其中mUserFixedVolume也是一個系統配置,一般情況下都不會使用固定值

mUseFixedVolume = mContext.getResources().getBoolean(
                com.android.internal.R.bool.config_useFixedVolume);
           

當mUseFixedVolume為false時,會調用isStreamAffectedByMute來判斷目前的streamType是否可以mute

public boolean isStreamAffectedByMute(int streamType) {
    return (mMuteAffectedStreams & ( << streamType)) != ;
}
           

mMuteAffectedStreams 的初始化是在前面的代碼裡面

mMuteAffectedStreams = System.getIntForUser(cr,
    System.MUTE_STREAMS_AFFECTED,
    (( << AudioSystem.STREAM_MUSIC)|
    ( << AudioSystem.STREAM_RING)|
    ( << AudioSystem.STREAM_SYSTEM)),
    UserHandle.USER_CURRENT);
           

System.MUTE_STREAMS_AFFECTED變量是在SettingsProvider裡面的DatabaseHelper類的loadVolumeLevels方法裡面初始化的

loadSetting(stmt, Settings.System.MUTE_STREAMS_AFFECTED,
          (( << AudioManager.STREAM_MUSIC) |
          ( << AudioManager.STREAM_RING) |
          ( << AudioManager.STREAM_NOTIFICATION) |
          ( << AudioManager.STREAM_SYSTEM)));
           

從這一段代碼可以看出,Notification是可以靜音的。

結論是:

audioManager.setStreamMute方法可以控制是否有通知聲音

參考連結:

http://m.blog.csdn.net/blog/Tsunami87/40535957

http://stackoverflow.com/questions/3004713/get-content-uri-from-file-path-in-android

http://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework?rq=1

http://www.cnblogs.com/andriod-html5/archive/2011/11/30/2539539.html

繼續閱讀