android音频系统(4):AudioService之音量管理

前言:AudioService这个系统服务包含或者使用了几乎所有与音频有关的内容,AudioService是音频系统在java层的大本营;

android音频系统,分为两个部分:数据流和策略;

数据流:描述了音频数据从数据源流向目的地的流程,之前我们分析的AudioTrack,AudioFlinger就是数据流;

策略:管理及控制数据流的路径与呈现方式,之前我们分析的AudioPolicyService,以及等会我们要分析的AudioService,它们都是策略得范畴;

 

AudioService:

①音频系统在java层中基本上不参与数据流的,AudioService这个系统服务包含或者使用了几乎所有与音频有关的内容,所以说AudioService是音频系统在java层的大本营;

②AudioManager拥有AudioService的Bp端,是AudioService在客户端的一个代理,几乎所有客户端对AudioManager进行的请求,最终都会交由AudioService实现;

③AudioService的功能实现依赖于AudioSystem类,AudioSystem无法实例化,它是java层到native层的代理,AudioService通过它与AudioPolicyService以及AudioFlinger进行通信;

 

今天我们分析的是,AudioService中的音量管理;

调整音量有好几种方式,我们慢慢看常见的两种:

1.音量管理:按下音量键

音量键被按下后,按键事件会一路派发给Acitivity,如果无人拦截并处理,承载当前Activity的显示PhoneWindow类的onKeyDown()以及onKeyUp()函数将会被处理,从而开始通过音量键调整音量的处理流程;

按照输入事件的派发策略,Window对象在事件的派发队列中位于Acitivity的后面,所以应用程序可以重写自己的Activity.onKeyDown()函数以截获音量键的消息,并将其用作其他的功能。比如说,在一个相机应用中,按下音量键所执行的动作是拍照而不是调节音量;

PhoneWindow.java
protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) {
    final KeyEvent.DispatcherState dispatcher =
            mDecor != null ? mDecor.getKeyDispatcherState() : null;

    switch (keyCode) {
        case KeyEvent.KEYCODE_VOLUME_UP:
        case KeyEvent.KEYCODE_VOLUME_DOWN:
        case KeyEvent.KEYCODE_VOLUME_MUTE: {
            //MediaController与视频播放器有关,使用VideoView+MediaController可实现视频播放器
            if (mMediaController != null) {
                int direction = 0;
                switch (keyCode) {
                    case KeyEvent.KEYCODE_VOLUME_UP:
                        direction = AudioManager.ADJUST_RAISE;
                        break;
                    case KeyEvent.KEYCODE_VOLUME_DOWN:
                        direction = AudioManager.ADJUST_LOWER;
                        break;
                    case KeyEvent.KEYCODE_VOLUME_MUTE:
                        direction = AudioManager.ADJUST_TOGGLE_MUTE;
                        break;
                }
                mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI);
            } else {
                //我们分析的是这句代码,后面有关于mVolumeControlStreamType的解析
                MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(
                        event, mVolumeControlStreamType, false);
            }
            return true;
        }
}

点评:将音量键输入按键事件传递给视频播放器或者MediaSessionLegacyHelper,我们这里分析的是MediaSessionLegacyHelper;

继续看代码:

public void sendVolumeKeyEvent(KeyEvent keyEvent, int stream, boolean musicOnly) {
    if (keyEvent == null) {
        return;
    }
    mSessionManager.dispatchVolumeKeyEvent(keyEvent, stream, musicOnly);
}

关于这个mSessionManager:mSessionManager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);

我们来到MediaSessionManager.java,继续看:

MediaSessionManager.java
public void dispatchVolumeKeyEvent(@NonNull KeyEvent keyEvent, int stream, boolean musicOnly) {
    try {
        mService.dispatchVolumeKeyEvent(keyEvent, stream, musicOnly);
    } catch (RemoteException e) {
        Log.e(TAG, "Failed to send volume key event.", e);
    }
}

这个mService是啥?

ISessionManager mService;
IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE);
mService = ISessionManager.Stub.asInterface(b);

通过分析我们知道mService是MediaSessionService;

继续看看MediaSessionService.dispatchVolumeKeyEvent()方法:

MediaSessionService.java
public void dispatchVolumeKeyEvent(KeyEvent keyEvent, int stream, boolean musicOnly) {
    /*这个方法是用来分析并处理音量键的长按和短按操作,如果有音量键的长按监听器,那么在音量键长按后,那么当前操作就不是调整音量,而是将事件派发给音量键的长按监听器*/
    if (keyEvent == null ||
            (keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_UP
                     && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_DOWN
                     && keyEvent.getKeyCode() != KeyEvent.KEYCODE_VOLUME_MUTE)) {
        Log.w(TAG, "Attempted to dispatch null or non-volume key event.");
        return;
    }

    final int pid = Binder.getCallingPid();
    final int uid = Binder.getCallingUid();
    final long token = Binder.clearCallingIdentity();

    if (DEBUG_KEY_EVENT) {
        Log.d(TAG, "dispatchVolumeKeyEvent, pid=" + pid + ", uid=" + uid + ", event="
                + keyEvent);
    }

    try {
        synchronized (mLock) {
            if (isGlobalPriorityActiveLocked()
                    || mCurrentFullUserRecord.mOnVolumeKeyLongPressListener == null) {
                //有活动的全局优先级会话或者没有音量键的长按监听,那就处理音量键的短按事件
                dispatchVolumeKeyEventLocked(keyEvent, stream, musicOnly);
            } else {
                // TODO: Consider the case when both volume up and down keys are pressed
                //       at the same time.
                if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
                    //第一次短按,在一段时间的延迟之后如果手指没有抬起,那就触发长按事件
                    if (keyEvent.getRepeatCount() == 0) {
                        //保存传递进来的参数副本
                        mCurrentFullUserRecord.mInitialDownVolumeKeyEvent =
                                KeyEvent.obtain(keyEvent);
                        mCurrentFullUserRecord.mInitialDownVolumeStream = stream;
                        mCurrentFullUserRecord.mInitialDownMusicOnly = musicOnly;
                        //一段延迟后触发长按事件,也就是触发onVolumeKeyLongPress方法,我们这里只分析短按事件,这个就不仔细剖析了
                        mHandler.sendMessageDelayed(
                                mHandler.obtainMessage(
                                        MessageHandler.MSG_VOLUME_INITIAL_DOWN,
                                        mCurrentFullUserRecord.mFullUserId, 0),
                                mLongPressTimeout);
                    }
                    //多次短按,或者本身就是一个长按事件时,直接触发长按事件
                    if (keyEvent.getRepeatCount() > 0 || keyEvent.isLongPress()) {
                        mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
                        if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null) {
                            dispatchVolumeKeyLongPressLocked(
                                    mCurrentFullUserRecord.mInitialDownVolumeKeyEvent);
                            // Mark that the key is already handled.
                            mCurrentFullUserRecord.mInitialDownVolumeKeyEvent = null;
                        }
                        dispatchVolumeKeyLongPressLocked(keyEvent);
                    }
                } else { // if up
                    mHandler.removeMessages(MessageHandler.MSG_VOLUME_INITIAL_DOWN);
                    if (mCurrentFullUserRecord.mInitialDownVolumeKeyEvent != null
                            && mCurrentFullUserRecord.mInitialDownVolumeKeyEvent
                                    .getDownTime() == keyEvent.getDownTime()) {
                        // Short-press. Should change volume.
                        dispatchVolumeKeyEventLocked(
                                mCurrentFullUserRecord.mInitialDownVolumeKeyEvent,
                                mCurrentFullUserRecord.mInitialDownVolumeStream,
                                mCurrentFullUserRecord.mInitialDownMusicOnly);
                        dispatchVolumeKeyEventLocked(keyEvent, stream, musicOnly);
                    } else {
                        dispatchVolumeKeyLongPressLocked(keyEvent);
                    }
                }
            }
        }
    } finally {
        Binder.restoreCallingIdentity(token);
    }
}

点评:这段代码是用来处理音量键的长按与短按的,我们这里只分析短按事件,继续看:

MediaSessionService.java
dispatchVolumeKeyEventLocked(keyEvent, stream, musicOnly);
private void dispatchVolumeKeyEventLocked(
        KeyEvent keyEvent, int stream, boolean musicOnly) {
    //注意我们传进来的参数,keyEvent为down事件,stream是音频类型,musicOnly为false
    boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
    boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
    int direction = 0;
    boolean isMute = false;
    //音量调整的方向
    switch (keyEvent.getKeyCode()) {
        case KeyEvent.KEYCODE_VOLUME_UP:
            direction = AudioManager.ADJUST_RAISE;
            break;
        case KeyEvent.KEYCODE_VOLUME_DOWN:
            //direction为音量调整的方向,1为增大,-1为减小
            direction = AudioManager.ADJUST_LOWER;
            break;
        case KeyEvent.KEYCODE_VOLUME_MUTE:
            isMute = true;
            break;
    }
    if (down || up) {
        //设置标签
        int flags = AudioManager.FLAG_FROM_KEY;
        if (musicOnly) {
            // This flag is used when the screen is off to only affect active media.
            flags |= AudioManager.FLAG_ACTIVE_MEDIA_ONLY;
        } else {
            // These flags are consistent with the home screen
            if (up) {
                //按下音量键后手指抬起,PLAY_SOUND,即播放按键音并且震动
                flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
            } else {
                //手指刚按下音量键,SHOW_UI,即显示UI并且震动,这个UI指的是音量调节框,后面来看到
                flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
            }
        }
        if (direction != 0) {
            //因为down事件已经处理了音量的调整,up事件不需要再做什么,播放个按键音声音就好
            if (up) {
                direction = 0;
            }
            //要开始调节音量了
            dispatchAdjustVolumeLocked(stream, direction, flags);
        } else if (isMute) {
            //按下了静音键
            if (down && keyEvent.getRepeatCount() == 0) {
                dispatchAdjustVolumeLocked(stream, AudioManager.ADJUST_TOGGLE_MUTE, flags);
            }
        }
    }
}

点评:这段代码的主要工作是,确定音量调整的方向以及设置按下音量键时所携带的动作;

重点来了:dispatchAdjustVolumeLocked();

MediaSessionService.java
private void dispatchAdjustVolumeLocked(int suggestedStream, int direction, int flags) {
    MediaSessionRecord session = isGlobalPriorityActiveLocked() ? mGlobalPrioritySession
            : mCurrentFullUserRecord.mPriorityStack.getDefaultVolumeSession();

    boolean preferSuggestedStream = false;
    if (isValidLocalStreamType(suggestedStream)
            && AudioSystem.isStreamActive(suggestedStream, 0)) {
        preferSuggestedStream = true;
    }
    if (DEBUG_KEY_EVENT) {
        Log.d(TAG, "Adjusting " + session + " by " + direction + ". flags="
                + flags + ", suggestedStream=" + suggestedStream
                + ", preferSuggestedStream=" + preferSuggestedStream);
    }
    if (session == null || preferSuggestedStream) {
        if ((flags & AudioManager.FLAG_ACTIVE_MEDIA_ONLY) != 0
                && !AudioSystem.isStreamActive(AudioManager.STREAM_MUSIC, 0)) {
            if (DEBUG) {
                Log.d(TAG, "No active session to adjust, skipping media only volume event");
            }
            return;
        }

        // Execute mAudioService.adjustSuggestedStreamVolume() on
        // handler thread of MediaSessionService.
        // This will release the MediaSessionService.mLock sooner and avoid
        // a potential deadlock between MediaSessionService.mLock and
        // ActivityManagerService lock.
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                try {
                    String packageName = getContext().getOpPackageName();
                    //重点来了,调用AudioService的adjustSuggestedStreamVolume()方法
                    mAudioService.adjustSuggestedStreamVolume(direction, suggestedStream,
                            flags, packageName, TAG);
                } catch (RemoteException e) {
                    Log.e(TAG, "Error adjusting default volume.", e);
                } catch (IllegalArgumentException e) {
                    Log.e(TAG, "Cannot adjust volume: direction=" + direction
                            + ", suggestedStream=" + suggestedStream + ", flags=" + flags,
                            e);
                }
            }
        });
    } else {
        session.adjustVolume(direction, flags, getContext().getPackageName(),
                Process.SYSTEM_UID, true);
    }
}

点评:上面的代码只有一个核心:

mAudioService.adjustSuggestedStreamVolume(direction, suggestedStream,flags, packageName, TAG);

分析下方法中参数的含义:

direction:表示了音量的调整方向,1位增大,-1为减小,0就是保持原音量;

suggestedStream:操作的音频类型,后面会分析到;

flags:音量键动作所携带的消息,比如播放按键音,显示音量调节框等;

packageName:应用包名;

 

mAudioService就是AudioService,来到AudioService.java,继续往下看:

AudioService.java
private void adjustSuggestedStreamVolume(int direction, int suggestedStreamType, int flags,
        String callingPackage, String caller, int uid) {
    final int streamType;
    //从这段代码可以看出,在AudioService中还有地方可以强行改变音量键控制的流类型,
    //后面有关于mUserSelectedVolumeControlStream的解析
    if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
        streamType = mVolumeControlStream;
    } else {
        //通过getActiveStreamType函数获取要控制的流类型,这里根据建议的流类型与AudioService的实际情况,返回一个值
        final int maybeActiveStreamType = getActiveStreamType(suggestedStreamType);
        final boolean activeForReal;
        if (maybeActiveStreamType == AudioSystem.STREAM_MUSIC) {
            activeForReal = isAfMusicActiveRecently(0);
        } else {
            activeForReal = AudioSystem.isStreamActive(maybeActiveStreamType, 0);
        }
        if (activeForReal || mVolumeControlStream == -1) {
            streamType = maybeActiveStreamType;
        } else {
            streamType = mVolumeControlStream;
        }
    }

    final boolean isMute = isMuteAdjust(direction);

    ensureValidStreamType(streamType);

    //这句代码等会过来分析
    final int resolvedStream = mStreamVolumeAlias[streamType];

    //只能在STREAM_RING音频类型下才能播放声音
    if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 &&
            resolvedStream != AudioSystem.STREAM_RING) {
        flags &= ~AudioManager.FLAG_PLAY_SOUND;
    }

    // For notifications/ring, show the ui before making any adjustments
    // Don't suppress mute/unmute requests
    if (mVolumeController.suppressAdjustment(resolvedStream, flags, isMute)) {
        direction = 0;
        flags &= ~AudioManager.FLAG_PLAY_SOUND;
        flags &= ~AudioManager.FLAG_VIBRATE;
        if (DEBUG_VOL) Log.d(TAG, "Volume controller suppressed adjustment");
    }

    //调用adjustStreamVolume()
    adjustStreamVolume(streamType, direction, flags, callingPackage, caller, uid);
}

点评:我们先把流程走完,有些细节后面一起分析;

继续看adjustStreamVolume()方法:

AudioService.java
private void adjustStreamVolume(int streamType, int direction, int flags,
        String callingPackage, String caller, int uid) {
    if (mUseFixedVolume) {
        return;
    }
    if (DEBUG_VOL) Log.d(TAG, "adjustStreamVolume() stream=" + streamType + ", dir=" + direction
            + ", flags=" + flags + ", caller=" + caller);

    ensureValidDirection(direction);
    ensureValidStreamType(streamType);

    //是否是静音调节
    boolean isMuteAdjust = isMuteAdjust(direction);

    if (isMuteAdjust && !isStreamAffectedByMute(streamType)) {
        return;
    }

    // use stream type alias here so that streams with same alias have the same behavior,
    // including with regard to silent mode control (e.g the use of STREAM_RING below and in
    // checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
    //获取streamType映射到的流类型
    int streamTypeAlias = mStreamVolumeAlias[streamType];

    //注意VolumeStreamState类
    VolumeStreamState streamState = mStreamStates[streamTypeAlias];

    final int device = getDeviceForStream(streamTypeAlias);

    //获取当前音量
    int aliasIndex = streamState.getIndex(device);
    boolean adjustVolume = true;
    int step;

    // skip a2dp absolute volume control request when the device
    // is not an a2dp device
    if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 &&
        (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
        return;
    }

    // If we are being called by the system (e.g. hardware keys) check for current user
    // so we handle user restrictions correctly.
    if (uid == android.os.Process.SYSTEM_UID) {
        uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
    }
    if (mAppOps.noteOp(STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage)
            != AppOpsManager.MODE_ALLOWED) {
        return;
    }

    // reset any pending volume command
    synchronized (mSafeMediaVolumeState) {
        mPendingVolumeCommand = null;
    }

    flags &= ~AudioManager.FLAG_FIXED_VOLUME;
    if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) &&
           ((device & mFixedVolumeDevices) != 0)) {
        flags |= AudioManager.FLAG_FIXED_VOLUME;

        // Always toggle between max safe volume and 0 for fixed volume devices where safe
        // volume is enforced, and max and 0 for the others.
        // This is simulated by stepping by the full allowed volume range
        if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
                (device & mSafeMediaVolumeDevices) != 0) {
            step = safeMediaVolumeIndex(device);
        } else {
            step = streamState.getMaxIndex();
        }
        if (aliasIndex != 0) {
            aliasIndex = step;
        }
    } else {
        // convert one UI step (+/-1) into a number of internal units on the stream alias
        //由于不同的流类型的音量调节范围不同,rescaleIndex用于将音量值的变化量从源流类型变换到目标流类型下
        step = rescaleIndex(10, streamType, streamTypeAlias);
    }

    // If either the client forces allowing ringer modes for this adjustment,
    // or the stream type is one that is affected by ringer modes
    //接下来就要开始有用的操作了
    if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
            (streamTypeAlias == getUiSoundsStreamType())) {
        int ringerMode = getRingerModeInternal();
        // do not vibrate if already in vibrate mode
        if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
            flags &= ~AudioManager.FLAG_VIBRATE;
        }
        // Check if the ringer mode handles this adjustment. If it does we don't
        // need to adjust the volume further.
        //变更情景模式,
        final int result = checkForRingerModeChange(aliasIndex, direction, step,
                streamState.mIsMuted, callingPackage, flags);
        //adjustVolume表示是否有必要继续设置音量值,因为在某些情况下,音量键是改变情景模式,而不是设置音量的,比如音量为0时,继续减小音量,此时就是改变情景模式为静音
        adjustVolume = (result & FLAG_ADJUST_VOLUME) != 0;
        // If suppressing a volume adjustment in silent mode, display the UI hint
        if ((result & AudioManager.FLAG_SHOW_SILENT_HINT) != 0) {
            flags |= AudioManager.FLAG_SHOW_SILENT_HINT;
        }
        // If suppressing a volume down adjustment in vibrate mode, display the UI hint
        if ((result & AudioManager.FLAG_SHOW_VIBRATE_HINT) != 0) {
            flags |= AudioManager.FLAG_SHOW_VIBRATE_HINT;
        }
    }
    // If the ringermode is suppressing media, prevent changes
    if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
        adjustVolume = false;
    }

    //取出调整前的音量值,这个值稍后被用在sendVolumeUpdate()方法中
    int oldIndex = mStreamStates[streamType].getIndex(device);

    if (adjustVolume && (direction != AudioManager.ADJUST_SAME)) {
        mAudioHandler.removeMessages(MSG_UNMUTE_STREAM);

        // Check if volume update should be send to AVRCP
        if (streamTypeAlias == AudioSystem.STREAM_MUSIC &&
            (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
            (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
            synchronized (mA2dpAvrcpLock) {
                if (mA2dp != null && mAvrcpAbsVolSupported) {
                    mA2dp.adjustAvrcpAbsoluteVolume(direction);
                }
            }
        }

        if (isMuteAdjust) {
            //这是静音情况,暂时忽略
            boolean state;
            if (direction == AudioManager.ADJUST_TOGGLE_MUTE) {
                state = !streamState.mIsMuted;
            } else {
                state = direction == AudioManager.ADJUST_MUTE;
            }
            if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
                setSystemAudioMute(state);
            }
            for (int stream = 0; stream < mStreamStates.length; stream++) {
                if (streamTypeAlias == mStreamVolumeAlias[stream]) {
                    if (!(readCameraSoundForced()
                                && (mStreamStates[stream].getStreamType()
                                    == AudioSystem.STREAM_SYSTEM_ENFORCED))) {
                        mStreamStates[stream].mute(state);
                    }
                }
            }
        } else if ((direction == AudioManager.ADJUST_RAISE) &&
                !checkSafeMediaVolume(streamTypeAlias, aliasIndex + step, device)) {
            Log.e(TAG, "adjustStreamVolume() safe volume index = " + oldIndex);
            mVolumeController.postDisplaySafeVolumeWarning(flags);
        } else if (streamState.adjustIndex(direction * step, device, caller)
                || streamState.mIsMuted) {
            //上面的判断中调用了streamState.adjustIndex()方法,表示如果音量值在调整之后没有发生变化,比如说达到了最大值,就不需要继续后面的操作了
            // Post message to set system volume (it in turn will post a
            // message to persist).
            //静音情况暂时忽略
            if (streamState.mIsMuted) {
                // Unmute the stream if it was previously muted
                if (direction == AudioManager.ADJUST_RAISE) {
                    // unmute immediately for volume up
                    streamState.mute(false);
                } else if (direction == AudioManager.ADJUST_LOWER) {
                    if (mIsSingleVolume) {
                        sendMsg(mAudioHandler, MSG_UNMUTE_STREAM, SENDMSG_QUEUE,
                                streamTypeAlias, flags, null, UNMUTE_STREAM_DELAY);
                    }
                }
            }
            //发送信息给mAudioHandler,这个消息将把音量设置到底层去,并将其存储到SettingsProvider中,后面的分析会看到
            sendMsg(mAudioHandler,
                    MSG_SET_DEVICE_VOLUME,
                    SENDMSG_QUEUE,
                    device,
                    0,
                    streamState,
                    0);
        }

        // Check if volume update should be sent to Hdmi system audio.
        int newIndex = mStreamStates[streamType].getIndex(device);
        if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
            setSystemAudioVolume(oldIndex, newIndex, getStreamMaxVolume(streamType), flags);
        }
        if (mHdmiManager != null) {
            synchronized (mHdmiManager) {
                // mHdmiCecSink true => mHdmiPlaybackClient != null
                if (mHdmiCecSink &&
                        streamTypeAlias == AudioSystem.STREAM_MUSIC &&
                        oldIndex != newIndex) {
                    synchronized (mHdmiPlaybackClient) {
                        int keyCode = (direction == -1) ? KeyEvent.KEYCODE_VOLUME_DOWN :
                                KeyEvent.KEYCODE_VOLUME_UP;
                        final long ident = Binder.clearCallingIdentity();
                        try {
                            mHdmiPlaybackClient.sendKeyEvent(keyCode, true);
                            mHdmiPlaybackClient.sendKeyEvent(keyCode, false);
                        } finally {
                            Binder.restoreCallingIdentity(ident);
                        }
                    }
                }
            }
        }
    }
    int index = mStreamStates[streamType].getIndex(device);
    //最后调用sendVolumeUpdate,通知外界音量值发生了变化
    sendVolumeUpdate(streamType, oldIndex, index, flags);
}

点评:上述代码的逻辑很多,我们逐一分析:

①在这个函数中,我们首先看到了这两句代码:

int streamTypeAlias = mStreamVolumeAlias[streamType];

VolumeStreamState streamState = mStreamStates[streamTypeAlias];

VolumeStreamState是什么?

我们知道,android的音量是依赖于某种流类型的,如果Android定义了N个流类型,AudioService就需要维护N个音量值与之相对应,所以AudioService提供了VolumeStreamState,为每一种流类型都分配了一个VolumeStreamState对象,VolumeStreamState保存与一个流类型所有音量相关的信息。并且以流类型的的值为索引,将它保存在一个名为mStreamStates的数组中;

②streamState.adjustIndex

streamState也就是VolumeStreamState,VolumeStreamState.adjustIndex()就是改变这个对象存储的音量值,不过它仅仅是改变了它的存储值,并没有把这个变化设置到底层;

我们来看下这个方法:

public boolean adjustIndex(int deltaIndex, int device, String caller) {
    //将现有的音量值加上变化量,然后调用setIndex进行设置
    return setIndex(getIndex(device) + deltaIndex, device, caller);
}

public boolean setIndex(int index, int device, String caller) {
    boolean changed = false;
    int oldIndex;
    synchronized (VolumeStreamState.class) {
        oldIndex = getIndex(device);
        index = getValidIndex(index);
        synchronized (mCameraSoundForced) {
            if ((mStreamType == AudioSystem.STREAM_SYSTEM_ENFORCED) && mCameraSoundForced) {
                index = mIndexMax;
            }
        }
        //保存设置的音量值,使用了Map
        mIndexMap.put(device, index);

        changed = oldIndex != index;
        //同时设置所有映射到当前流类型的其他流的音量
        final boolean currentDevice = (device == getDeviceForStream(mStreamType));
        final int numStreamTypes = AudioSystem.getNumStreamTypes();
        for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
            final VolumeStreamState aliasStreamState = mStreamStates[streamType];
            if (streamType != mStreamType &&
                    mStreamVolumeAlias[streamType] == mStreamType &&
                    (changed || !aliasStreamState.hasIndexForDevice(device))) {
                final int scaledIndex = rescaleIndex(index, mStreamType, streamType);
                aliasStreamState.setIndex(scaledIndex, device, caller);
                if (currentDevice) {
                    aliasStreamState.setIndex(scaledIndex,
                            getDeviceForStream(streamType), caller);
                }
            }
        }
    }
    if (changed) {
        //加5是为了四舍五入,除10,因为10的整数容易操作
        oldIndex = (oldIndex + 5) / 10;
        index = (index + 5) / 10;
        //发送广播
        mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, index);
        mVolumeChanged.putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, oldIndex);
        mVolumeChanged.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE_ALIAS,
                mStreamVolumeAlias[mStreamType]);
        sendBroadcastToAll(mVolumeChanged);
    }
    return changed;
}

点评:这个函数有如下工作:

(1)首先是保存设置的音量值,音量值与设备相关联,对同一种流类型来说,在不同的音频设备下将会有不同的音量值;

(2)再就是对流映射的处理。既然A→B,那么在设置B的音量的同时要改变A的音量;

可以看出,VolumeStreamState.adjustIndex()除了更新自己所保存的音量值外,没有做其他的事情;

 

再来看sendMsg(),发送MSG_SET_DEVICE_VOLUME消息;

这个消息是发送给mAudioHandler,mAudioHandler是运行在AudioService主线程上的Handler,我们看下mAudioHandler处理这个消息的setDeviceVolume函数:

private void setDeviceVolume(VolumeStreamState streamState, int device) {

    synchronized (VolumeStreamState.class) {
        //这个函数就是调用AudioSystem.setStreamVolumeIndex,将音量设置到底层的AudioFlinger
        streamState.applyDeviceVolume_syncVSS(device);

        // Apply change to all streams using this one as alias
        //处理流映射的情况
        int numStreamTypes = AudioSystem.getNumStreamTypes();
        for (int streamType = numStreamTypes - 1; streamType >= 0; streamType--) {
            if (streamType != streamState.mStreamType &&
                    mStreamVolumeAlias[streamType] == streamState.mStreamType) {
                int streamDevice = getDeviceForStream(streamType);
                if ((device != streamDevice) && mAvrcpAbsVolSupported &&
                        ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0)) {
                    mStreamStates[streamType].applyDeviceVolume_syncVSS(device);
                }
                mStreamStates[streamType].applyDeviceVolume_syncVSS(streamDevice);
            }
        }
    }
    /*发送消息给mAudioHandler,其处理函数将会调用persitVolume()函数,这将会把音量的设置信息存储到SettingsProvider中;
    AudioService在初始化时,将会从SettingsProvider中将音量设置读取出来并进行设置 */
    sendMsg(mAudioHandler,
            MSG_PERSIST_VOLUME,
            SENDMSG_QUEUE,
            device,
            0,
            streamState,
            PERSIST_DELAY);
}

再看下mAudioHandler处理MSG_PERSIST_VOLUME消息的persistVolume函数:

private void persistVolume(VolumeStreamState streamState, int device) {
    if (mUseFixedVolume) {
        return;
    }
    if (mIsSingleVolume && (streamState.mStreamType != AudioSystem.STREAM_MUSIC)) {
        return;
    }
    if (streamState.hasValidSettingsName()) {
        //将音量值写进SettingProvider
        System.putIntForUser(mContentResolver,
                streamState.getSettingNameForDevice(device),
                (streamState.getIndex(device) + 5)/ 10,
                UserHandle.USER_CURRENT);
    }
}

点评:sendMsg是个异步操作,消息是串行的,由于是先返回值会操作,就意味着执行结果可能会有延迟,即获取的音量值没问题,但是手机发出的声音音量值大小并没有变化;

 

③sendVolumeUpdate()分析

sendVolumeUpdate()的工作内容,就是通知外界音量值发生了变化,其中有一项就是通知音量调节通知框,因为6.0以后的代码逻辑改变比较大,我们后面再分析这个;

 

这样,音量调节的流程就结束了,接下来做个总结:

①音量键处理流程的发起者是PhoneWindow;

②MediaSessionService仅仅起到代理的作用;

③AudioService接受MediaSessionService的调用请求,操作VolumeStreamState的实例进行音量的设置;

④VolumeStreamState负责保存音量设置,并且提供了将音量设置到底层的方法;

⑤AudioService负责将设置结果以广播的形式通知外界;

 

OK!音量调节的流程说完了,接下来要说遗留的细节代码了!

①在PhoneWindow.java的onKeyDown方法中,有这么一句代码:

MediaSessionLegacyHelper.getHelper(getContext()).sendVolumeKeyEvent(event, mVolumeControlStreamType, false);

这里的mVolumeControlStreamType是什么?

mVolumeControlStreamType:它指的是要改变哪一种流类型的音量,adnroid的音量控制与流密不可分,每种流类型都独立的拥有自己的音量设置,如音乐音量,通话音量就是相互独立的。mVolumeControlStreamType它是从哪来的呢?

在Activity中有这么一个函数,通过它可以来指定显示这个Activity时音量键所控制的流类型;

Activity.java

Activity.java
public final void setVolumeControlStream(int streamType) {
    getWindow().setVolumeControlStream(streamType);
}

getWindow()就是用于显示当前Activity的PhoneWindow,再回到PhoneWindow.setVolumeControlStream()方法:

PhoneWindow.java
public void setVolumeControlStream(int streamType) {
    mVolumeControlStreamType = streamType;
}

这就是mVolumeControlStreamType的由来;

我们应该能看出,这个设置被绑定到Activity的Window上,在不同Activity之间切换时,接收按键事件的Window也会随之切换,所以应用不需要去考虑在其生命周期中音量键所控制的流类型的切换问题;

②在AudioService.adjustStreamVolume()中有这么两句代码:

int streamTypeAlias = mStreamVolumeAlias[streamType];
VolumeStreamState streamState = mStreamStates[streamTypeAlias];

我们先看mStreamVolumeAlias是什么?

找到mStreamVolumeAlias被赋值的其中一处:

mStreamVolumeAlias = STREAM_VOLUME_ALIAS_DEFAULT;
private final int[] STREAM_VOLUME_ALIAS_DEFAULT = new int[] {
    AudioSystem.STREAM_VOICE_CALL,      // STREAM_VOICE_CALL
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM
    AudioSystem.STREAM_RING,            // STREAM_RING
    AudioSystem.STREAM_MUSIC,           // STREAM_MUSIC
    AudioSystem.STREAM_ALARM,           // STREAM_ALARM
    AudioSystem.STREAM_RING,            // STREAM_NOTIFICATION
    AudioSystem.STREAM_BLUETOOTH_SCO,   // STREAM_BLUETOOTH_SCO
    AudioSystem.STREAM_RING,            // STREAM_SYSTEM_ENFORCED
    AudioSystem.STREAM_RING,            // STREAM_DTMF
    AudioSystem.STREAM_MUSIC,           // STREAM_TTS
    AudioSystem.STREAM_MUSIC            // STREAM_ACCESSIBILITY
};

看到这里我们知道了,mStreamVolumeAlias是一个数组,数组里面依次保存着音频流类型;所以resolvedStream也只不过是从mStreamVolumeAlias数组中获取到相应的流类型而已;

mStreamStates也是用来保存VolumeStreamState的数组,streamState是从数组中获取到相应的VolumeStreamState;

初看:这种写法不是多次一举吗,直接VolumeStreamState streamState = mStreamStates[streamType]不就可以了吗?

其实不然:

这种设计是为了满足为了所谓的“将铃声音量用作音乐音量”这种需求,如果碰到这个需求,我们就可以这么处理:

mStreamVolumeAlias[AudioSystem.STREAM_MUSIC] = AudioSystem.STREAM_RING;

当我们在操作音乐类型的流时,实际上是在操作铃声类型的流;

这种设计是不是很棒,是不是很nice,其实HaspMap也可以做嘛,键是源流类型,值是目标流类型;

当真的有“将铃声音量用作音乐音量”这种奇葩需求时:

我们需要根据实际情况给mStreamVolumeAlias数组赋值,AudioService也给它提供了几种在不同情形下的不同赋值,它的赋值跟硬件平台有关;

 

③在AudioService.adjustSuggestedStreamVolume()中有这么一句:

if (mUserSelectedVolumeControlStream) { // implies mVolumeControlStream != -1
	streamType = mVolumeControlStream;
}

mUserSelectedVolumeControlStream是什么?只要它等于true,就强制调整音量的类型就是它!

看看它是在哪被赋值的?

public void forceVolumeControlStream(int streamType, IBinder cb) {
    if (DEBUG_VOL) { Log.d(TAG, String.format("forceVolumeControlStream(%d)", streamType)); }
    synchronized(mForceControlStreamLock) {
        //mVolumeControlStream != -1,便将mUserSelectedVolumeControlStream设为true
        if (mVolumeControlStream != -1 && streamType != -1) {
            mUserSelectedVolumeControlStream = true;
        }
        //给mVolumeControlStream 赋值
        mVolumeControlStream = streamType;
        if (mVolumeControlStream == -1) {
            if (mForceControlStreamClient != null) {
                mForceControlStreamClient.release();
                mForceControlStreamClient = null;
            }
            mUserSelectedVolumeControlStream = false;
        } else {
            if (mForceControlStreamClient != null) {
                mForceControlStreamClient.release();
            }
            mForceControlStreamClient = new ForceControlStreamClient(cb);
        }
    }
}

我们可以看到,mUserSelectedVolumeControlStream以及mVolumeControlStream 是在forceVolumeControlStream()方法中被赋值的,而forceVolumeControlStream()方法是由VolumePanel调用的,VolumePanel就是我们按下音量键后的那个音量调节通知框,如果我们在VolumePanel显示时改变的是铃声类型的音量,那么它会调用forceVolumeControlStream强制后续的音量键操作固定为促使它显示的那个流类型,也就是铃声类型,并在它关闭时取消这个强制设置,即设置mVolumeControlStream为-1,这个在后面分析VolumePanel时会看到;

 

2.音量管理:setStreamVolume()

还有一种直接调整音量的接口:AudioManager.setStreamVolume();

AudioManager.java
public void setStreamVolume(int streamType, int index, int flags) {
    final IAudioService service = getService();
    Log.d(TAG, "setStreamVolume: StreamType = " + streamType + ", index = " + index);
    try {
        service.setStreamVolume(streamType, index, flags, getContext().getOpPackageName());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

我们知道,service就是AudioService,继续:;

AudioService.java
public void setStreamVolume(int streamType, int index, int flags, String callingPackage) {
    if ((streamType == AudioManager.STREAM_ACCESSIBILITY) && !canChangeAccessibilityVolume()) {
        Log.w(TAG, "Trying to call setStreamVolume() for a11y without"
                + " CHANGE_ACCESSIBILITY_VOLUME  callingPackage=" + callingPackage);
        return;
    }
    mVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
            index/*val1*/, flags/*val2*/, callingPackage));
    setStreamVolume(streamType, index, flags, callingPackage, callingPackage,
            Binder.getCallingUid());
}

private void setStreamVolume(int streamType, int index, int flags, String callingPackage,
        String caller, int uid) {
    if (DEBUG_VOL) {
        Log.d(TAG, "setStreamVolume(stream=" + streamType+", index=" + index
                + ", calling=" + callingPackage + ")");
    }
    if (mUseFixedVolume) {
        return;
    }

    //判读流类型的有效性
    ensureValidStreamType(streamType);
    int streamTypeAlias = mStreamVolumeAlias[streamType];
    //获取相应的VolumeStreamState
    VolumeStreamState streamState = mStreamStates[streamTypeAlias];

    //获取当前流将使用哪一个音频设备进行播放
    final int device = getDeviceForStream(streamType);
    int oldIndex;

    // skip a2dp absolute volume control request when the device
    // is not an a2dp device
    if ((device & AudioSystem.DEVICE_OUT_ALL_A2DP) == 0 &&
        (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) != 0) {
        return;
    }
    // If we are being called by the system (e.g. hardware keys) check for current user
    // so we handle user restrictions correctly.
    if (uid == android.os.Process.SYSTEM_UID) {
        uid = UserHandle.getUid(getCurrentUserId(), UserHandle.getAppId(uid));
    }
    if (mAppOps.noteOp(STREAM_VOLUME_OPS[streamTypeAlias], uid, callingPackage)
            != AppOpsManager.MODE_ALLOWED) {
        return;
    }

    if (isAndroidNPlus(callingPackage)
            && wouldToggleZenMode(getNewRingerMode(streamTypeAlias, index, flags))
            && !mNm.isNotificationPolicyAccessGrantedForPackage(callingPackage)) {
        throw new SecurityException("Not allowed to change Do Not Disturb state");
    }

    if (!volumeAdjustmentAllowedByDnd(streamTypeAlias, flags)) {
        return;
    }

    synchronized (mSafeMediaVolumeState) {
        // reset any pending volume command
        mPendingVolumeCommand = null;

        //获取流当前的音量
        oldIndex = streamState.getIndex(device);

        //将原流类型下的音量值映射到目标流类型下的音量值;
        //因为不同流类型的音量值刻度不一样,所以需要进行转换
        index = rescaleIndex(index * 10, streamType, streamTypeAlias);

        if (streamTypeAlias == AudioSystem.STREAM_MUSIC &&
            (device & AudioSystem.DEVICE_OUT_ALL_A2DP) != 0 &&
            (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
            synchronized (mA2dpAvrcpLock) {
                if (mA2dp != null && mAvrcpAbsVolSupported) {
                    mA2dp.setAvrcpAbsoluteVolume(index / 10);
                }
            }
        }

        if (streamTypeAlias == AudioSystem.STREAM_MUSIC) {
            setSystemAudioVolume(oldIndex, index, getStreamMaxVolume(streamType), flags);
        }

        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
        if ((streamTypeAlias == AudioSystem.STREAM_MUSIC) &&
                ((device & mFixedVolumeDevices) != 0)) {
            flags |= AudioManager.FLAG_FIXED_VOLUME;

            // volume is either 0 or max allowed for fixed volume devices
            if (index != 0) {
                if (mSafeMediaVolumeState == SAFE_MEDIA_VOLUME_ACTIVE &&
                        (device & mSafeMediaVolumeDevices) != 0) {
                    index = safeMediaVolumeIndex(device);
                } else {
                    index = streamState.getMaxIndex();
                }
            }
        }

        if (!checkSafeMediaVolume(streamTypeAlias, index, device)) {
            mVolumeController.postDisplaySafeVolumeWarning(flags);
            mPendingVolumeCommand = new StreamVolumeCommand(
                                                streamType, index, flags, device);
        } else {
            //调用onSetStreamVolume
            onSetStreamVolume(streamType, index, flags, device, caller);
            //获取设置的结果
            index = mStreamStates[streamType].getIndex(device);
        }
    }
    //广播通知
    sendVolumeUpdate(streamType, oldIndex, index, flags);
}

我们来看一下:onSetStreamVolume()

private void onSetStreamVolume(int streamType, int index, int flags, int device,
        String caller) {
    final int stream = mStreamVolumeAlias[streamType];
    //调用setStreamVolumeInt
    setStreamVolumeInt(stream, index, device, false, caller);
    // setting volume on ui sounds stream type also controls silent mode
    if (((flags & AudioManager.FLAG_ALLOW_RINGER_MODES) != 0) ||
            (stream == getUiSoundsStreamType())) {
        setRingerMode(getNewRingerMode(stream, index, flags),
                TAG + ".onSetStreamVolume", false /*external*/);
    }
   //如果调节后的音量为0了,那就让某种流的音量静音
    mStreamStates[stream].mute(index == 0);
}

private void setStreamVolumeInt(int streamType,
                                int index,
                                int device,
                                boolean force,
                                String caller) {
    VolumeStreamState streamState = mStreamStates[streamType];

    //调用streamState.setIndex
    if (streamState.setIndex(index, device, caller) || force) {
        //如果setIndex返回true,或者force为true,就在这里向mAudioHandler发送通知
        sendMsg(mAudioHandler,
                MSG_SET_DEVICE_VOLUME,
                SENDMSG_QUEUE,
                device,
                0,
                streamState,
                0);
    }
}

使用setStreamVolume()调整音量,跟点击音量键控制音质,其实也是差不多的,就不再过多的叙述了;

 

3.音量管理的总结

①AudioService音量管理的核心是VolumeStreamState,它保存了一个流类型所有的音量信息;

②VolumeStreamState保存了运行时的音量信息,而音量的生效则是在底层AudioFlinger完成的;

所以进行音量设置需要做两件事情:更新VolumeStreamState存储的音量值,设置音量到Audio底层系统;

  • 7
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Android开发案例驱动教程》 配套代码。 注: 由于第12,13,14章代码太大,无法上传到一个包中。 这三节代码会放到其他压缩包中。 作者:关东升,赵志荣 Java或C++程序员转变成为Android程序员 采用案例驱动模式展开讲解知识点,即介绍案例->案例涉及技术->展开知识点->总结的方式 本书作者从事多年一线开发和培训,讲解知识点力求细致,深入浅出 目 录 前言 第1章 Android操作系统概述 1 1.1 Android历史介绍 1 1.2 Android架构 1 1.3 Android平台介绍 2 1.4 现有智能手机操作系统比较 4 第2章 Android开发环境搭建 5 2.1 Eclipse和ADT插件 5 2.1.1 Eclipse安装 5 2.1.2 ADT插件 6 2.2 Android SDK 8 2.2.1 Android SDK的获得 8 2.2.2 Android SDK版本说明 10 2.2.3 ADT配置 10 2.3 Android开发模拟器 11 2.3.1 创建模拟器 11 2.3.2 启动模拟器 13 2.3.3 键盘映射与模拟器控制 13 2.3.4 横屏与竖屏切换 14 第3章 第一个Android程序 15 3.1 HelloAndroid 15 3.1.1 在Eclipse中创建项目 15 3.1.2 编写程序项目代码 17 3.1.3 运行HelloAndroid 18 3.1.4 Android工程目录 19 3.1.5 AndroidManifest.xml文件 21 3.2 Android中的组件介绍 22 3.3 使用Android SDK帮助 23 3.3.1 Android SDK API文档 23 3.3.2 Android SDK开发指南 24 3.3.3 Android SDK samples 24 3.4 使用DDMS帮助调试程序 26 3.4.1 启动DDMS 26 3.4.2 Device 28 3.4.3 Emulator Control 29 3.4.4 File Explorer 30 3.4.5 LogCat 31 3.5 使用ADB帮助调试程序 33 3.5.1 查询模拟器实例和设备 34 3.5.2 进入shell 34 3.5.3 导入导出文件 35 3.6 应用程序的打包、安装和卸载 37 3.6.1 应用程序打包 37 3.6.2 应用程序安装 40 3.6.3 应用程序卸载 40 本章小结 42 第4章 UI基础知识 43 4.1 Android UI组件概述 43 4.1.1 View 43 4.1.2 ViewGroup 44 4.1.3 布局管理器 44 4.2 UI设计工具 44 4.2.1 DroidDraw工具 44 4.2.2 ADT插件UI设计工具 46 4.3 事件处理模型 47 4.3.1 接口实现事件处理模型 47 4.3.2 内部类事件处理模型 49 4.3.3 匿名内部类事件处理模型 51 4.4 Activity中的常用事件 53 4.4.1 触摸事件 53 4.4.2 键盘事件 55 4.5 菜单 57 4.5.1 文本菜单 57 4.5.2 图片文本菜单 59 本章小结 60 第5章 UI基础控件 61 5.1 按钮 61 5.1.1 Button 62 5.1.2 ImageButton 63 5.1.3 ToggleButton 64 5.2 TextView 64 5.3 EditText 65 5.4 RadioButton和RadioGroup 66 5.4.1 RadioButton 66 5.4.2 RadioGroup 67 5.5 CheckBox 68 5.6 ImageView 70 5.7 Progress Bar 70 5.7.1 条状进度条 71 5.7.2 圆形进度条 73 5.7.3 对话框进度条 74 5.7.4 标题栏中进度条 75 5.8 SeekBar 76 5.9 RatingBar 78 本章小结 82 第6章 UI高级控件 83 6.1 列表类控件 83 6.1.1 Adapter概念 83 6.1.2 AutoComplete 84 6.1.3 Spinner 87 6.1.4 ListView 90 6.1.5 GridView 96 6.1.6 Gallery 99 6.2 Toast 103 6.2.1 文本类型 103 6.2.2 图片类型 104 6.2.3 复合类型 105 6.2.4 自定义显示位置Toast 106 6.3 对话框 107 6.3.1 文本信息对话框 107 6.3.2 简单列表项对话框 109 6.3.3 单选项列表项对话框 111 6.3.4 复选框列表项对话框 113 6.3.5 复杂布局列表项对话框 115 6.4 Android国际化和本地化 118 本章小结 121 第7章 UI布局 122 7.1 FrameLayout 122 7.1.1 TextSwitcher 124 7.1.2 ImageSwitcher 126 7.1.3 DatePicker 129 7.1.4 TimePicker 131 7.1.5 ScrollView 133 7.1.6 选项卡 134 7.2 LinearLayout 138 7.3 RelativeLayout 139 7.4 AbsoluteLayout 141 7.5 TableLayout 143 7.6 布局嵌套 146 7.7 屏幕旋转 152 本章小结 154 第8章 多线程 155 8.1 多线程案例--计时器 155 8.2 线程概念 156 8.2.1 进程概念 156 8.2.2 线程概念 156 8.3 Java中的线程 157 8.3.1 Java中的实现线程体方式1 157 8.3.2 Java中的实现线程体方式2 160 8.3.3 Java中的实现线程体方式3 162 8.4 Android中的线程 163 8.4.1 Android线程应用中的问题与分析 164 8.4.2 Message和MessageQueue 169 8.4.3 Handler 169 8.4.4 Looper和HandlerThread 172 本章小结 178 第9章 Activity和Intent 179 9.1 Activity 179 9.1.1 创建Activity 179 9.1.2 Activity生命周期 180 9.2 Intent 183 9.2.1 显式Intent 184 9.2.2 隐式Intent 186 9.2.3 匹配组件 186 9.3 多Activity之间跳转 188 9.3.1 多个Activity之间数据传递 189 9.3.2 跳转与返回 192 9.3.3 任务与标志 196 9.4 Android系统内置Intent 199 本章小结 201 第10章 数据存储 203 10.1 健康助手案例 203 10.2 Android数据存储概述 205 10.3 本地文件 205 10.3.1 访问SD卡 207 10.3.2 访问应用文件目录 212 10.4 SQLite数据库 216 10.4.1 SQLite数据类型 216 10.4.2 Android平台下管理SQLite数据库 216 10.5 编写访问SQLite数据库组件 220 10.5.1 DBHelper类 220 10.5.2 数据插入 222 10.5.3 数据删除 224 10.5.4 数据修改 224 10.5.5 数据查询 227 10.6 案例重构 229 10.6.1 系统架构设计 229 10.6.2 重构数据访问层 230 10.7 为案例增加参数设置功能 238 10.7.1 Shared Preferences 240 10.7.2 Preferences控件介绍 243 10.7.3 使用Preferences控件的案例 248 本章小结 250 第11章 Content Provider 251 11.1 Content Provider概述 251 11.2 Content URI 252 11.2.1 Content URI含义 252 11.2.2 内置的Content URI 253 11.3 通过Content Provider访问联系人 253 11.3.1 查询联系人 255 11.3.2 通过联系人ID查询联系人的Email 258 11.3.3 按照过滤条件查询Email 259 11.3.4 查询联系人的电话 261 11.4 通过Content Provider访问通话记录 262 11.4.1 查询通话记录 262 11.4.2 按照过滤条件查询通话记录 264 11.5 通过Content Provider访问短信 266 11.6 自定义Content Provider实现数据访问 269 11.6.1 编写Content Provider 269 11.6.2 在不同的应用中调用Content Provider 277 11.6.3 重构Content Provider调用 278 本章小结 281 第12章 多媒体 282 12.1 多媒体文件介绍 282 12.1.1 音频多媒体文件介绍 282 12.1.2 视频多媒体文件介绍 283 12.2 Android音频播放 284 12.2.1 Android音频/视频播放状态 284 12.2.2 音频播放案例介绍 286 12.2.3 资源音频文件播放 287 12.2.4 本地音频文件播放 291 12.2.5 网络音频文件播放 292 12.2.6 完善案例其他功能 293 12.3 Android音频录制 303 12.3.1 Android音频/视频录制状态 303 12.3.2 音频录制案例介绍 303 12.3.3 音频录制案例实现 305 12.4 Android视频播放 309 12.4.1 视频播放案例 309 12.4.2 采用MediaPlayer类播放视频 310 12.4.3 使用VideoView控件重构案例 315 本章小结 316 第13章 Service 317 13.1 Service概述 317 13.1.1 本地Service生命周期 317 13.1.2 远程Service生命周期 318 13.2 本地Service 319 13.2.1 本地Service案例 319 13.2.2 编写AudioService 320 13.2.3 调用Service 322 13.2.4 重构案例 323 13.3 远程Service 325 13.3.1 远程Service调用原理 325 13.3.2 远程Service案例 326 13.3.3 设计AIDL文件 327 13.3.4 编写AudioService 331 13.3.5 调用远程Service 336 13.3.6 组件间参数传递 343 本章小结 347 第14章 Broadcast Receiver和Notification 348 14.1 Broadcast Receiver 348 14.1.1 音频播放案例 349 14.1.2 编写音频播放Broadcast Receiver 350 14.1.3 注册音频播放Broadcast Receiver 351 14.1.4 接收系统的广播 353 14.1.5 MP3下载服务案例 353 14.2 Notification 358 14.2.1 完善MP3下载服务案例 358 14.2.2 完善音频播放案例 363 14.2.3 其他形式的Notification 369 本章小结 371 第15章 云端应用 372 15.1 典型云端应用--城市天气信息服务 372 15.2 网络通信技术与实现 374 15.2.1 网络通信技术介绍 376 15.2.2 Java URL类实现方式 377 15.2.3 Apache HttpClient实现方式 378 15.3 数据交换格式 380 15.3.1 纯文本格式 381 15.3.2 XML格式 381 15.3.3 JSON格式 385 15.4 自定义服务器端程序实例 387 15.4.1 Java Servlet概述 387 15.4.2 编写城市信息服务的Servlet 388 15.4.3 编写城市天气服务的Servlet 393 15.4.4 再次探讨HttpClient的POST请求 395 15.5 云端应用案例优化 400 本章小结 404 第16章 Google Map和定位服务 405 16.1 MyMap服务系统案例 405 16.2 Android Google Map 406 16.2.1 申请Google Map Android API Key 407 16.2.2 编写Android Google Map骨架程序 409 16.2.3 控制地图 412 16.2.4 地图的显示模式 416 16.2.5 地图的图层 419 16.2.6 查询与定位 422 16.3 Android定位服务 430 16.3.1 开启定位服务 431 16.3.2 模拟测试 433 16.3.3 GPS与Google Map结合 435 16.4 案例重构 437 16.4.1 重构"定位查询"方法 438 16.4.2 重构"查询周围"方法 440 本章小结 443 第17章 Android通信应用 444 17.1 电话应用开发 444 17.1.1 拨打电话功能 444 17.1.2 呼入电话状态 446 17.2 短信和彩信应用开发 450 17.2.1 Android内置的发送短信/彩信功能 450 17.2.2 自己编写发送文本内容的短信 452 17.2.3 自己编写接收文本内容的短信 458 17.2.4 自己编写发送二进制内容的短信 459 17.2.5 自己编写接收二进制内容的短信 461 17.3 蓝牙通信 463 17.3.1 Android 2 BluetoothChat案例 464 17.3.2 Android 2 蓝牙API介绍 464 17.3.3 TCP Socket与蓝牙Socket的区别 465 17.3.4 BluetoothChat中的类 466 17.3.5 初始化本地蓝牙设备 467 17.3.6 查找蓝牙设备 471 17.3.7 管理连接 476 17.3.8 互相之间的通信 480 17.4 WiFi通信 484 17.4.1 管理WiFi 484 17.4.2 扫描热点 487 17.4.3 Socket通信 489

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

renshuguo123723

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值