Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package org.chromium.media; | 5 package org.chromium.media; |
| 6 | 6 |
| 7 import android.bluetooth.BluetoothAdapter; | |
| 8 import android.bluetooth.BluetoothManager; | |
| 9 import android.content.BroadcastReceiver; | 7 import android.content.BroadcastReceiver; |
| 10 import android.content.ContentResolver; | 8 import android.content.ContentResolver; |
| 11 import android.content.Context; | 9 import android.content.Context; |
| 12 import android.content.Intent; | 10 import android.content.Intent; |
| 13 import android.content.IntentFilter; | 11 import android.content.IntentFilter; |
| 14 import android.content.pm.PackageManager; | 12 import android.content.pm.PackageManager; |
| 15 import android.database.ContentObserver; | 13 import android.database.ContentObserver; |
| 16 import android.media.AudioFormat; | 14 import android.media.AudioFormat; |
| 17 import android.media.AudioManager; | 15 import android.media.AudioManager; |
| 18 import android.media.AudioRecord; | 16 import android.media.AudioRecord; |
| 19 import android.media.AudioTrack; | 17 import android.media.AudioTrack; |
| 20 import android.os.Build; | 18 import android.os.Build; |
| 21 import android.os.Handler; | 19 import android.os.Handler; |
| 22 import android.os.Looper; | 20 import android.os.Looper; |
| 23 import android.os.Process; | |
| 24 import android.provider.Settings; | 21 import android.provider.Settings; |
| 25 import android.util.Log; | 22 import android.util.Log; |
| 26 | 23 |
| 27 import org.chromium.base.CalledByNative; | 24 import org.chromium.base.CalledByNative; |
| 28 import org.chromium.base.JNINamespace; | 25 import org.chromium.base.JNINamespace; |
| 29 | 26 |
| 30 import java.util.ArrayList; | 27 import java.util.ArrayList; |
| 31 import java.util.Arrays; | 28 import java.util.Arrays; |
| 32 import java.util.List; | 29 import java.util.List; |
| 33 | 30 |
| 34 @JNINamespace("media") | 31 @JNINamespace("media") |
| 35 class AudioManagerAndroid { | 32 class AudioManagerAndroid { |
| 36 private static final String TAG = "AudioManagerAndroid"; | 33 private static final String TAG = "AudioManagerAndroid"; |
| 37 | 34 |
| 38 // Set to true to enable debug logs. Always check in as false. | 35 // Set to true to enable debug logs. Always check in as false. |
| 39 private static final boolean DEBUG = false; | 36 private static final boolean DEBUG = true; |
| 40 | 37 |
| 41 /** Simple container for device information. */ | 38 /** Simple container for device information. */ |
| 42 private static class AudioDeviceName { | 39 private static class AudioDeviceName { |
| 43 private final int mId; | 40 private final int mId; |
| 44 private final String mName; | 41 private final String mName; |
| 45 | 42 |
| 46 private AudioDeviceName(int id, String name) { | 43 private AudioDeviceName(int id, String name) { |
| 47 mId = id; | 44 mId = id; |
| 48 mName = name; | 45 mName = name; |
| 49 } | 46 } |
| 50 | 47 |
| 51 @CalledByNative("AudioDeviceName") | 48 @CalledByNative("AudioDeviceName") |
| 52 private String id() { return String.valueOf(mId); } | 49 private String id() { return String.valueOf(mId); } |
| 53 | 50 |
| 54 @CalledByNative("AudioDeviceName") | 51 @CalledByNative("AudioDeviceName") |
| 55 private String name() { return mName; } | 52 private String name() { return mName; } |
| 56 } | 53 } |
| 57 | 54 |
| 58 // Supported audio device types. | 55 // Supported audio device types. |
| 56 private static final int DEVICE_DEFAULT = -2; | |
| 59 private static final int DEVICE_INVALID = -1; | 57 private static final int DEVICE_INVALID = -1; |
| 60 private static final int DEVICE_SPEAKERPHONE = 0; | 58 private static final int DEVICE_SPEAKERPHONE = 0; |
| 61 private static final int DEVICE_WIRED_HEADSET = 1; | 59 private static final int DEVICE_WIRED_HEADSET = 1; |
| 62 private static final int DEVICE_EARPIECE = 2; | 60 private static final int DEVICE_EARPIECE = 2; |
| 63 private static final int DEVICE_BLUETOOTH_HEADSET = 3; | 61 private static final int DEVICE_BLUETOOTH_HEADSET = 3; |
| 64 private static final int DEVICE_COUNT = 4; | 62 private static final int DEVICE_COUNT = 4; |
| 65 | 63 |
| 66 // Maps audio device types to string values. This map must be in sync | 64 // Maps audio device types to string values. This map must be in sync |
| 67 // with the device types above. | 65 // with the device types above. |
| 68 // TODO(henrika): add support for proper detection of device names and | 66 // TODO(henrika): add support for proper detection of device names and |
| 69 // localize the name strings by using resource strings. | 67 // localize the name strings by using resource strings. |
| 70 private static final String[] DEVICE_NAMES = new String[] { | 68 private static final String[] DEVICE_NAMES = new String[] { |
| 71 "Speakerphone", | 69 "Speakerphone", |
| 72 "Wired headset", // With or without microphone | 70 "Wired headset", // With or without microphone |
| 73 "Headset earpiece", // Only available on mobile phones | 71 "Headset earpiece", // Only available on mobile phones |
| 74 "Bluetooth headset", | 72 "Bluetooth headset", |
| 75 }; | 73 }; |
| 76 | 74 |
| 77 // List of valid device types. | 75 // List of valid device types. |
| 78 private static final Integer[] VALID_DEVICES = new Integer[] { | 76 private static final Integer[] VALID_DEVICES = new Integer[] { |
| 79 DEVICE_SPEAKERPHONE, | 77 DEVICE_SPEAKERPHONE, |
| 80 DEVICE_WIRED_HEADSET, | 78 DEVICE_WIRED_HEADSET, |
| 81 DEVICE_EARPIECE, | 79 DEVICE_EARPIECE, |
| 82 DEVICE_BLUETOOTH_HEADSET, | 80 DEVICE_BLUETOOTH_HEADSET, |
| 83 }; | 81 }; |
| 84 | 82 |
| 85 // The device does not have any audio device. | |
| 86 static final int STATE_NO_DEVICE_SELECTED = 0; | |
| 87 // The speakerphone is on and an associated microphone is used. | |
| 88 static final int STATE_SPEAKERPHONE_ON = 1; | |
| 89 // The phone's earpiece is on and an associated microphone is used. | |
| 90 static final int STATE_EARPIECE_ON = 2; | |
| 91 // A wired headset (with or without a microphone) is plugged in. | |
| 92 static final int STATE_WIRED_HEADSET_ON = 3; | |
| 93 // The audio stream is being directed to a Bluetooth headset. | |
| 94 static final int STATE_BLUETOOTH_ON = 4; | |
| 95 // We've requested that the audio stream be directed to Bluetooth, but | |
| 96 // have not yet received a response from the framework. | |
| 97 static final int STATE_BLUETOOTH_TURNING_ON = 5; | |
| 98 // We've requested that the audio stream stop being directed to | |
| 99 // Bluetooth, but have not yet received a response from the framework. | |
| 100 static final int STATE_BLUETOOTH_TURNING_OFF = 6; | |
| 101 // TODO(henrika): document the valid state transitions. | |
| 102 | |
| 103 // Use 44.1kHz as the default sampling rate. | 83 // Use 44.1kHz as the default sampling rate. |
| 104 private static final int DEFAULT_SAMPLING_RATE = 44100; | 84 private static final int DEFAULT_SAMPLING_RATE = 44100; |
| 105 // Randomly picked up frame size which is close to return value on N4. | 85 // Randomly picked up frame size which is close to return value on N4. |
| 106 // Return this value when getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) | 86 // Return this value when getProperty(PROPERTY_OUTPUT_FRAMES_PER_BUFFER) |
| 107 // fails. | 87 // fails. |
| 108 private static final int DEFAULT_FRAME_PER_BUFFER = 256; | 88 private static final int DEFAULT_FRAME_PER_BUFFER = 256; |
| 109 | 89 |
| 110 private final AudioManager mAudioManager; | 90 private final AudioManager mAudioManager; |
| 111 private final Context mContext; | 91 private final Context mContext; |
| 112 private final long mNativeAudioManagerAndroid; | 92 private final long mNativeAudioManagerAndroid; |
| 113 | 93 |
| 114 private boolean mHasBluetoothPermission = false; | 94 private int mSavedAudioMode = AudioManager.MODE_INVALID; |
| 95 | |
| 115 private boolean mIsInitialized = false; | 96 private boolean mIsInitialized = false; |
| 116 private boolean mSavedIsSpeakerphoneOn; | 97 private boolean mSavedIsSpeakerphoneOn; |
| 117 private boolean mSavedIsMicrophoneMute; | 98 private boolean mSavedIsMicrophoneMute; |
| 118 | 99 |
| 119 private Integer mAudioDeviceState = STATE_NO_DEVICE_SELECTED; | 100 // Id of the currently active audio device, i.e., audio is routed to |
| 101 // this device. | |
| 102 private int mActiveAudioDevice = DEVICE_INVALID; | |
| 103 // Id of the currently selected audio device. Can only be modified by | |
| 104 // call to setDevice(). | |
| 105 private int mSelectedAudioDevice = DEVICE_INVALID; | |
| 120 | 106 |
| 121 // Lock to protect |mAudioDevices| which can be accessed from the main | 107 // Lock to protect |mAudioDevices| which can be accessed from the main |
| 122 // thread and the audio manager thread. | 108 // thread and the audio manager thread. |
| 123 private final Object mLock = new Object(); | 109 private final Object mLock = new Object(); |
| 124 | 110 |
| 125 // Contains a list of currently available audio devices. | 111 // Contains a list of currently available audio devices. |
| 126 private boolean[] mAudioDevices = new boolean[DEVICE_COUNT]; | 112 private boolean[] mAudioDevices = new boolean[DEVICE_COUNT]; |
| 127 | 113 |
| 128 private final ContentResolver mContentResolver; | 114 private final ContentResolver mContentResolver; |
| 129 private SettingsObserver mSettingsObserver = null; | 115 private SettingsObserver mSettingsObserver = null; |
| 130 private SettingsObserverThread mSettingsObserverThread = null; | 116 private SettingsObserverThread mSettingsObserverThread = null; |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
Change the type to be HandlerThread. You'll also
henrika (OOO until Aug 14)
2013/12/12 15:39:15
Done.
| |
| 131 private int mCurrentVolume; | 117 private int mCurrentVolume; |
| 132 private final Object mSettingsObserverLock = new Object(); | 118 private final Object mSettingsObserverLock = new Object(); |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
This lock is not needed.
henrika (OOO until Aug 14)
2013/12/12 15:39:15
Done.
| |
| 133 | 119 |
| 134 // Broadcast receiver for wired headset intent broadcasts. | 120 // Broadcast receiver for wired headset intent broadcasts. |
| 135 private BroadcastReceiver mWiredHeadsetReceiver; | 121 private BroadcastReceiver mWiredHeadsetReceiver; |
| 136 | 122 |
| 137 /** Construction */ | 123 /** Construction */ |
| 138 @CalledByNative | 124 @CalledByNative |
| 139 private static AudioManagerAndroid createAudioManagerAndroid( | 125 private static AudioManagerAndroid createAudioManagerAndroid( |
| 140 Context context, | 126 Context context, |
| 141 long nativeAudioManagerAndroid) { | 127 long nativeAudioManagerAndroid) { |
| 142 return new AudioManagerAndroid(context, nativeAudioManagerAndroid); | 128 return new AudioManagerAndroid(context, nativeAudioManagerAndroid); |
| 143 } | 129 } |
| 144 | 130 |
| 145 private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) { | 131 private AudioManagerAndroid(Context context, long nativeAudioManagerAndroid) { |
| 146 mContext = context; | 132 mContext = context; |
| 147 mNativeAudioManagerAndroid = nativeAudioManagerAndroid; | 133 mNativeAudioManagerAndroid = nativeAudioManagerAndroid; |
| 148 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_S ERVICE); | 134 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_S ERVICE); |
| 149 mContentResolver = mContext.getContentResolver(); | 135 mContentResolver = mContext.getContentResolver(); |
| 150 } | 136 } |
| 151 | 137 |
| 152 /** | 138 /** |
| 153 * Saves the initial speakerphone and microphone state. | 139 * Saves the initial speakerphone and microphone state. |
| 154 * Populates the list of available audio devices and registers receivers | 140 * Populates the list of available audio devices and registers receivers |
| 155 * for broadcasted intents related to wired headset and bluetooth devices. | 141 * for broadcasted intents related to wired headset and bluetooth devices. |
| 156 */ | 142 */ |
| 157 @CalledByNative | 143 @CalledByNative |
| 158 public void init() { | 144 public void init() { |
| 145 if (DEBUG) logd("init"); | |
| 159 if (mIsInitialized) | 146 if (mIsInitialized) |
| 160 return; | 147 return; |
| 161 | 148 |
| 162 synchronized (mLock) { | 149 for (int i = 0; i < DEVICE_COUNT; ++i) { |
| 163 for (int i = 0; i < DEVICE_COUNT; ++i) { | 150 mAudioDevices[i] = false; |
| 164 mAudioDevices[i] = false; | |
| 165 } | |
| 166 } | 151 } |
| 167 | 152 |
| 168 // Store microphone mute state and speakerphone state so it can | |
| 169 // be restored when closing. | |
| 170 mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn(); | |
| 171 mSavedIsMicrophoneMute = mAudioManager.isMicrophoneMute(); | |
| 172 | |
| 173 // Always enable speaker phone by default. This state might be reset | |
| 174 // by the wired headset receiver when it gets its initial sticky | |
| 175 // intent, if any. | |
| 176 setSpeakerphoneOn(true); | |
| 177 mAudioDeviceState = STATE_SPEAKERPHONE_ON; | |
| 178 | |
| 179 // Initialize audio device list with things we know is always available. | 153 // Initialize audio device list with things we know is always available. |
| 180 synchronized (mLock) { | 154 if (hasEarpiece()) { |
| 181 if (hasEarpiece()) { | 155 mAudioDevices[DEVICE_EARPIECE] = true; |
| 182 mAudioDevices[DEVICE_EARPIECE] = true; | |
| 183 } | |
| 184 mAudioDevices[DEVICE_SPEAKERPHONE] = true; | |
| 185 } | 156 } |
| 157 mAudioDevices[DEVICE_SPEAKERPHONE] = true; | |
| 186 | 158 |
| 187 // Register receiver for broadcasted intents related to adding/ | 159 // Register receiver for broadcasted intents related to adding/ |
| 188 // removing a wired headset (Intent.ACTION_HEADSET_PLUG). | 160 // removing a wired headset (Intent.ACTION_HEADSET_PLUG). |
| 189 // Also starts routing to the wired headset/headphone if one is | |
| 190 // already attached (can be overridden by a Bluetooth headset). | |
| 191 registerForWiredHeadsetIntentBroadcast(); | 161 registerForWiredHeadsetIntentBroadcast(); |
| 192 | 162 |
| 193 // Start routing to Bluetooth if there's a connected device. | |
| 194 // TODO(henrika): the actual routing part is not implemented yet. | |
| 195 // All we do currently is to detect if BT headset is attached or not. | |
| 196 initBluetooth(); | |
| 197 | |
| 198 mIsInitialized = true; | |
| 199 | |
| 200 mSettingsObserverThread = new SettingsObserverThread(); | 163 mSettingsObserverThread = new SettingsObserverThread(); |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
Change lines 163-171 to:
mSettingsObserverThread
| |
| 201 mSettingsObserverThread.start(); | 164 mSettingsObserverThread.start(); |
| 202 synchronized (mSettingsObserverLock) { | 165 synchronized (mSettingsObserverLock) { |
| 203 try { | 166 try { |
| 204 mSettingsObserverLock.wait(); | 167 mSettingsObserverLock.wait(); |
| 205 } catch (InterruptedException e) { | 168 } catch (InterruptedException e) { |
| 206 Log.e(TAG, "unregisterHeadsetReceiver exception: " + e.getMessag e()); | 169 loge("Object.wait exception: " + e.getMessage()); |
| 207 } | 170 } |
| 208 } | 171 } |
| 172 | |
| 173 mIsInitialized = true; | |
| 209 } | 174 } |
| 210 | 175 |
| 211 /** | 176 /** |
| 212 * Unregister all previously registered intent receivers and restore | 177 * Unregister all previously registered intent receivers and restore |
| 213 * the stored state (stored in {@link #init()}). | 178 * the stored state (stored in {@link #init()}). |
| 214 */ | 179 */ |
| 215 @CalledByNative | 180 @CalledByNative |
| 216 public void close() { | 181 public void close() { |
| 182 if (DEBUG) logd("close"); | |
| 217 if (!mIsInitialized) | 183 if (!mIsInitialized) |
| 218 return; | 184 return; |
| 219 | 185 |
| 220 if (mSettingsObserverThread != null) { | 186 if (mSettingsObserverThread != null) { |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
change lines 186-192 to:
mSettingsObserverThread.q
henrika (OOO until Aug 14)
2013/12/12 15:39:15
Done.
| |
| 221 mSettingsObserverThread = null; | 187 mSettingsObserverThread = null; |
| 222 } | 188 } |
| 223 if (mSettingsObserver != null) { | 189 if (mSettingsObserver != null) { |
| 224 mContentResolver.unregisterContentObserver(mSettingsObserver); | 190 mContentResolver.unregisterContentObserver(mSettingsObserver); |
| 225 mSettingsObserver = null; | 191 mSettingsObserver = null; |
| 226 } | 192 } |
| 227 | 193 |
| 228 unregisterForWiredHeadsetIntentBroadcast(); | 194 unregisterForWiredHeadsetIntentBroadcast(); |
| 229 | 195 |
| 230 // Restore previously stored audio states. | |
| 231 setMicrophoneMute(mSavedIsMicrophoneMute); | |
| 232 setSpeakerphoneOn(mSavedIsSpeakerphoneOn); | |
| 233 | |
| 234 mIsInitialized = false; | 196 mIsInitialized = false; |
| 235 } | 197 } |
| 236 | 198 |
| 199 /** | |
| 200 * Saves current audio mode and sets audio mode to MODE_IN_COMMUNICATION | |
| 201 * if input parameter is true. Restores saved audio mode if input parameter | |
| 202 * is false. | |
| 203 */ | |
| 237 @CalledByNative | 204 @CalledByNative |
| 238 public void setMode(int mode) { | 205 public void setCommunicationAudioModeOn(boolean on) { |
| 239 try { | 206 if (DEBUG) logd("setCommunicationAudioModeOn(" + on + ")"); |
| 240 mAudioManager.setMode(mode); | 207 |
| 241 } catch (SecurityException e) { | 208 if (on) { |
| 242 Log.e(TAG, "setMode exception: " + e.getMessage()); | 209 if (mSavedAudioMode != AudioManager.MODE_INVALID) { |
| 243 logDeviceInfo(); | 210 logwtf("Audio mode has already been set!"); |
| 211 return; | |
| 212 } | |
| 213 | |
| 214 // Store the current audio mode the first time we try to | |
| 215 // switch to communication mode. | |
| 216 try { | |
| 217 mSavedAudioMode = mAudioManager.getMode(); | |
| 218 } catch (SecurityException e) { | |
| 219 logwtf("getMode exception: " + e.getMessage()); | |
| 220 logDeviceInfo(); | |
| 221 } | |
| 222 | |
| 223 // Store microphone mute state and speakerphone state so it can | |
| 224 // be restored when closing. | |
| 225 mSavedIsSpeakerphoneOn = mAudioManager.isSpeakerphoneOn(); | |
| 226 mSavedIsMicrophoneMute = mAudioManager.isMicrophoneMute(); | |
| 227 | |
| 228 try { | |
| 229 mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); | |
| 230 } catch (SecurityException e) { | |
| 231 logwtf("setMode exception: " + e.getMessage()); | |
| 232 logDeviceInfo(); | |
| 233 } | |
| 234 } else { | |
| 235 if (mSavedAudioMode == AudioManager.MODE_INVALID) { | |
| 236 logwtf("Audio mode has not yet been set!"); | |
| 237 return; | |
| 238 } | |
| 239 | |
| 240 // Restore previously stored audio states. | |
| 241 setMicrophoneMute(mSavedIsMicrophoneMute); | |
| 242 setSpeakerphoneOn(mSavedIsSpeakerphoneOn); | |
| 243 | |
| 244 // Restore the mode that was used before we switched to | |
| 245 // communication mode. | |
| 246 try { | |
| 247 mAudioManager.setMode(mSavedAudioMode); | |
| 248 } catch (SecurityException e) { | |
| 249 logwtf("setMode exception: " + e.getMessage()); | |
| 250 logDeviceInfo(); | |
| 251 } | |
| 252 mSavedAudioMode = AudioManager.MODE_INVALID; | |
| 244 } | 253 } |
| 245 } | 254 } |
| 246 | 255 |
| 247 /** | 256 /** |
| 248 * Activates, i.e., starts routing audio to, the specified audio device. | 257 * Activates, i.e., starts routing audio to, the specified audio device. |
| 249 * | 258 * |
| 250 * @param deviceId Unique device ID (integer converted to string) | 259 * @param deviceId Unique device ID (integer converted to string) |
| 251 * representing the selected device. This string is empty if the so-called | 260 * representing the selected device. This string is empty if the so-called |
| 252 * default device is selected. | 261 * default device is selected. |
| 253 */ | 262 */ |
| 254 @CalledByNative | 263 @CalledByNative |
| 255 public void setDevice(String deviceId) { | 264 boolean setDevice(String deviceId) { |
| 256 boolean devices[] = null; | 265 boolean devices[] = null; |
| 257 synchronized (mLock) { | 266 synchronized (mLock) { |
| 258 devices = mAudioDevices.clone(); | 267 devices = mAudioDevices.clone(); |
| 259 } | 268 } |
| 260 if (deviceId.isEmpty()) { | 269 if (deviceId.isEmpty()) { |
| 261 logd("setDevice: default"); | 270 if (DEBUG) logd("setDevice: default"); |
| 262 // Use a special selection scheme if the default device is selected. | 271 int defaultDevice = selectDefaultDevice(devices); |
| 263 // The "most unique" device will be selected; Bluetooth first, then | 272 setAudioDevice(defaultDevice); |
| 264 // wired headset and last the speaker phone. | 273 mSelectedAudioDevice = DEVICE_DEFAULT; |
| 265 if (devices[DEVICE_BLUETOOTH_HEADSET]) { | 274 return true; |
| 266 // TODO(henrika): possibly need improvements here if we are | 275 } else if (isNumeric(deviceId)) { |
| 267 // in a STATE_BLUETOOTH_TURNING_OFF state. | 276 if (DEBUG) logd("setDevice: " + deviceId); |
| 268 setAudioDevice(DEVICE_BLUETOOTH_HEADSET); | |
| 269 } else if (devices[DEVICE_WIRED_HEADSET]) { | |
| 270 setAudioDevice(DEVICE_WIRED_HEADSET); | |
| 271 } else { | |
| 272 setAudioDevice(DEVICE_SPEAKERPHONE); | |
| 273 } | |
| 274 } else { | |
| 275 logd("setDevice: " + deviceId); | |
| 276 // A non-default device is specified. Verify that it is valid | 277 // A non-default device is specified. Verify that it is valid |
| 277 // device, and if so, start using it. | 278 // device, and if so, start using it. |
| 278 List<Integer> validIds = Arrays.asList(VALID_DEVICES); | 279 List<Integer> validIds = Arrays.asList(VALID_DEVICES); |
| 279 Integer id = Integer.valueOf(deviceId); | 280 Integer id = Integer.valueOf(deviceId); |
| 280 if (validIds.contains(id)) { | 281 if (validIds.contains(id) && mAudioDevices[id.intValue()]) { |
| 282 mSelectedAudioDevice = id.intValue(); | |
| 281 setAudioDevice(id.intValue()); | 283 setAudioDevice(id.intValue()); |
| 282 } else { | 284 return true; |
| 283 loge("Invalid device ID!"); | |
| 284 } | 285 } |
| 285 } | 286 } |
| 287 loge("Invalid device ID: " + deviceId); | |
| 288 return false; | |
| 286 } | 289 } |
| 287 | 290 |
| 288 /** | 291 /** |
| 289 * @return the current list of available audio devices. | 292 * @return the current list of available audio devices. |
| 290 * Note that this call does not trigger any update of the list of devices, | 293 * Note that this call does not trigger any update of the list of devices, |
| 291 * it only copies the current state in to the output array. | 294 * it only copies the current state in to the output array. |
| 292 */ | 295 */ |
| 293 @CalledByNative | 296 @CalledByNative |
| 294 public AudioDeviceName[] getAudioInputDeviceNames() { | 297 public AudioDeviceName[] getAudioInputDeviceNames() { |
| 295 synchronized (mLock) { | 298 synchronized (mLock) { |
| 296 List<String> devices = new ArrayList<String>(); | 299 List<String> devices = new ArrayList<String>(); |
| 297 AudioDeviceName[] array = new AudioDeviceName[getNumOfAudioDevicesWi thLock()]; | 300 AudioDeviceName[] array = new AudioDeviceName[getNumOfAudioDevicesWi thLock()]; |
| 298 int i = 0; | 301 int i = 0; |
| 299 for (int id = 0; id < DEVICE_COUNT; ++id) { | 302 for (int id = 0; id < DEVICE_COUNT; ++id) { |
| 300 if (mAudioDevices[id]) { | 303 if (mAudioDevices[id]) { |
| 301 array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]); | 304 array[i] = new AudioDeviceName(id, DEVICE_NAMES[id]); |
| 302 devices.add(DEVICE_NAMES[id]); | 305 devices.add(DEVICE_NAMES[id]); |
| 303 i++; | 306 i++; |
| 304 } | 307 } |
| 305 } | 308 } |
| 306 logd("getAudioInputDeviceNames: " + devices); | 309 if (DEBUG) logd("getAudioInputDeviceNames: " + devices); |
| 307 return array; | 310 return array; |
| 308 } | 311 } |
| 309 } | 312 } |
| 310 | 313 |
| 311 @CalledByNative | 314 @CalledByNative |
| 312 private int getNativeOutputSampleRate() { | 315 private int getNativeOutputSampleRate() { |
| 313 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.J ELLY_BEAN_MR1) { | 316 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.J ELLY_BEAN_MR1) { |
| 314 String sampleRateString = mAudioManager.getProperty( | 317 String sampleRateString = mAudioManager.getProperty( |
| 315 AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); | 318 AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); |
| 316 return (sampleRateString == null ? | 319 return (sampleRateString == null ? |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 405 | 408 |
| 406 /** | 409 /** |
| 407 * Registers receiver for the broadcasted intent when a wired headset is | 410 * Registers receiver for the broadcasted intent when a wired headset is |
| 408 * plugged in or unplugged. The received intent will have an extra | 411 * plugged in or unplugged. The received intent will have an extra |
| 409 * 'state' value where 0 means unplugged, and 1 means plugged. | 412 * 'state' value where 0 means unplugged, and 1 means plugged. |
| 410 */ | 413 */ |
| 411 private void registerForWiredHeadsetIntentBroadcast() { | 414 private void registerForWiredHeadsetIntentBroadcast() { |
| 412 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); | 415 IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); |
| 413 | 416 |
| 414 /** | 417 /** |
| 415 * Receiver which handles changes in wired headset availablilty. | 418 * Receiver which handles changes in wired headset availability: |
| 419 * updates the list of devices; | |
| 420 * updates the active device if a device selection has been made. | |
| 416 */ | 421 */ |
| 417 mWiredHeadsetReceiver = new BroadcastReceiver() { | 422 mWiredHeadsetReceiver = new BroadcastReceiver() { |
| 418 private static final int STATE_UNPLUGGED = 0; | 423 private static final int STATE_UNPLUGGED = 0; |
| 419 private static final int STATE_PLUGGED = 1; | 424 private static final int STATE_PLUGGED = 1; |
| 420 private static final int HAS_NO_MIC = 0; | 425 private static final int HAS_NO_MIC = 0; |
| 421 private static final int HAS_MIC = 1; | 426 private static final int HAS_MIC = 1; |
| 422 | 427 |
| 423 @Override | 428 @Override |
| 424 public void onReceive(Context context, Intent intent) { | 429 public void onReceive(Context context, Intent intent) { |
| 425 String action = intent.getAction(); | 430 String action = intent.getAction(); |
| 426 if (!action.equals(Intent.ACTION_HEADSET_PLUG)) { | 431 if (!action.equals(Intent.ACTION_HEADSET_PLUG)) { |
| 427 return; | 432 return; |
| 428 } | 433 } |
| 429 int state = intent.getIntExtra("state", STATE_UNPLUGGED); | 434 int state = intent.getIntExtra("state", STATE_UNPLUGGED); |
| 430 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); | 435 int microphone = intent.getIntExtra("microphone", HAS_NO_MIC); |
| 431 String name = intent.getStringExtra("name"); | 436 String name = intent.getStringExtra("name"); |
| 432 logd("==> onReceive: s=" + state | 437 if (DEBUG) { |
| 438 logd("==> onReceive: s=" + state | |
| 433 + ", m=" + microphone | 439 + ", m=" + microphone |
| 434 + ", n=" + name | 440 + ", n=" + name |
| 435 + ", sb=" + isInitialStickyBroadcast()); | 441 + ", sb=" + isInitialStickyBroadcast()); |
| 436 | 442 } |
| 437 switch (state) { | 443 switch (state) { |
| 438 case STATE_UNPLUGGED: | 444 case STATE_UNPLUGGED: |
| 439 synchronized (mLock) { | 445 synchronized (mLock) { |
| 440 // Wired headset and earpiece are mutually exclusive . | 446 // Wired headset and earpiece are mutually exclusive . |
| 441 mAudioDevices[DEVICE_WIRED_HEADSET] = false; | 447 mAudioDevices[DEVICE_WIRED_HEADSET] = false; |
| 442 if (hasEarpiece()) { | 448 if (hasEarpiece()) { |
| 443 mAudioDevices[DEVICE_EARPIECE] = true; | 449 mAudioDevices[DEVICE_EARPIECE] = true; |
| 444 } | 450 } |
| 445 } | 451 } |
| 446 // If wired headset was used before it was unplugged, | |
| 447 // switch to speaker phone. If it was not in use; just | |
| 448 // log the change. | |
| 449 if (mAudioDeviceState == STATE_WIRED_HEADSET_ON) { | |
| 450 setAudioDevice(DEVICE_SPEAKERPHONE); | |
| 451 } else { | |
| 452 reportUpdate(); | |
| 453 } | |
| 454 break; | 452 break; |
| 455 case STATE_PLUGGED: | 453 case STATE_PLUGGED: |
| 456 synchronized (mLock) { | 454 synchronized (mLock) { |
| 457 // Wired headset and earpiece are mutually exclusive . | 455 // Wired headset and earpiece are mutually exclusive . |
| 458 mAudioDevices[DEVICE_WIRED_HEADSET] = true; | 456 mAudioDevices[DEVICE_WIRED_HEADSET] = true; |
| 459 mAudioDevices[DEVICE_EARPIECE] = false; | 457 mAudioDevices[DEVICE_EARPIECE] = false; |
| 460 setAudioDevice(DEVICE_WIRED_HEADSET); | |
| 461 } | 458 } |
| 462 break; | 459 break; |
| 463 default: | 460 default: |
| 464 loge("Invalid state!"); | 461 loge("Invalid state!"); |
| 465 break; | 462 break; |
| 466 } | 463 } |
| 464 | |
| 465 // Update the existing device selection, but only if a specific | |
| 466 // device has already been selected explicitly. | |
| 467 boolean deviceHasBeenSelected = false; | |
| 468 synchronized (mLock) { | |
| 469 deviceHasBeenSelected = (mSelectedAudioDevice != DEVICE_INVA LID); | |
| 470 } | |
| 471 if (deviceHasBeenSelected) { | |
| 472 updateDeviceActivation(); | |
| 473 } | |
| 467 } | 474 } |
| 468 }; | 475 }; |
| 469 | 476 |
| 470 // Note: the intent we register for here is sticky, so it'll tell us | 477 // Note: the intent we register for here is sticky, so it'll tell us |
| 471 // immediately what the last action was (plugged or unplugged). | 478 // immediately what the last action was (plugged or unplugged). |
| 472 // It will enable us to set the speakerphone correctly. | 479 // It will enable us to set the speakerphone correctly. |
| 473 mContext.registerReceiver(mWiredHeadsetReceiver, filter); | 480 mContext.registerReceiver(mWiredHeadsetReceiver, filter); |
| 474 } | 481 } |
| 475 | 482 |
| 476 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ | 483 /** Unregister receiver for broadcasted ACTION_HEADSET_PLUG intent. */ |
| 477 private void unregisterForWiredHeadsetIntentBroadcast() { | 484 private void unregisterForWiredHeadsetIntentBroadcast() { |
| 478 mContext.unregisterReceiver(mWiredHeadsetReceiver); | 485 mContext.unregisterReceiver(mWiredHeadsetReceiver); |
| 479 mWiredHeadsetReceiver = null; | 486 mWiredHeadsetReceiver = null; |
| 480 } | 487 } |
| 481 | 488 |
| 482 /** | 489 /** |
| 483 * Check if Bluetooth device is connected, register Bluetooth receiver | |
| 484 * and start routing to Bluetooth if a device is connected. | |
| 485 * TODO(henrika): currently only supports the detecion part at startup. | |
| 486 */ | |
| 487 private void initBluetooth() { | |
| 488 // Bail out if we don't have the required permission. | |
| 489 mHasBluetoothPermission = mContext.checkPermission( | |
| 490 android.Manifest.permission.BLUETOOTH, | |
| 491 Process.myPid(), | |
| 492 Process.myUid()) == PackageManager.PERMISSION_GRANTED; | |
| 493 if (!mHasBluetoothPermission) { | |
| 494 loge("BLUETOOTH permission is missing!"); | |
| 495 return; | |
| 496 } | |
| 497 | |
| 498 // To get a BluetoothAdapter representing the local Bluetooth adapter, | |
| 499 // when running on JELLY_BEAN_MR1 (4.2) and below, call the static | |
| 500 // getDefaultAdapter() method; when running on JELLY_BEAN_MR2 (4.3) and | |
| 501 // higher, retrieve it through getSystemService(String) with | |
| 502 // BLUETOOTH_SERVICE. | |
| 503 // Note: Most methods require the BLUETOOTH permission. | |
| 504 BluetoothAdapter btAdapter = null; | |
| 505 if (android.os.Build.VERSION.SDK_INT <= | |
| 506 android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { | |
| 507 // Use static method for Android 4.2 and below to get the | |
| 508 // BluetoothAdapter. | |
| 509 btAdapter = BluetoothAdapter.getDefaultAdapter(); | |
| 510 } else { | |
| 511 // Use BluetoothManager to get the BluetoothAdapter for | |
| 512 // Android 4.3 and above. | |
| 513 BluetoothManager btManager = (BluetoothManager) mContext.getSystemSe rvice( | |
| 514 Context.BLUETOOTH_SERVICE); | |
| 515 btAdapter = btManager.getAdapter(); | |
| 516 } | |
| 517 | |
| 518 if (btAdapter != null && | |
| 519 // android.bluetooth.BluetoothAdapter.getProfileConnectionState | |
| 520 // requires BLUETOOTH permission. | |
| 521 android.bluetooth.BluetoothProfile.STATE_CONNECTED == | |
| 522 btAdapter.getProfileConnectionState( | |
| 523 android.bluetooth.BluetoothProfile.HEADSET)) { | |
| 524 synchronized (mLock) { | |
| 525 mAudioDevices[DEVICE_BLUETOOTH_HEADSET] = true; | |
| 526 } | |
| 527 // TODO(henrika): ensure that we set the active audio | |
| 528 // device to Bluetooth (not trivial). | |
| 529 setAudioDevice(DEVICE_BLUETOOTH_HEADSET); | |
| 530 } | |
| 531 } | |
| 532 | |
| 533 /** | |
| 534 * Changes selection of the currently active audio device. | 490 * Changes selection of the currently active audio device. |
| 535 * | 491 * |
| 536 * @param device Specifies the selected audio device. | 492 * @param device Specifies the selected audio device. |
| 537 */ | 493 */ |
| 538 public void setAudioDevice(int device) { | 494 public void setAudioDevice(int device) { |
| 495 if (device == mActiveAudioDevice) { | |
| 496 if (DEBUG) logd("setAudioDevice: " + device + " is already active"); | |
| 497 return; | |
| 498 } | |
| 539 switch (device) { | 499 switch (device) { |
| 540 case DEVICE_BLUETOOTH_HEADSET: | 500 case DEVICE_BLUETOOTH_HEADSET: |
| 541 // TODO(henrika): add support for turning on an routing to | 501 // TODO(henrika): add support for turning on an routing to |
| 542 // BT here. | 502 // BT here. |
| 543 if (DEBUG) logd("--- TO BE IMPLEMENTED ---"); | 503 if (DEBUG) logd("--- TO BE IMPLEMENTED ---"); |
| 544 break; | 504 break; |
| 545 case DEVICE_SPEAKERPHONE: | 505 case DEVICE_SPEAKERPHONE: |
| 546 // TODO(henrika): turn off BT if required. | 506 // TODO(henrika): turn off BT if required. |
| 547 mAudioDeviceState = STATE_SPEAKERPHONE_ON; | 507 mActiveAudioDevice = DEVICE_SPEAKERPHONE; |
| 548 setSpeakerphoneOn(true); | 508 setSpeakerphoneOn(true); |
| 549 break; | 509 break; |
| 550 case DEVICE_WIRED_HEADSET: | 510 case DEVICE_WIRED_HEADSET: |
| 551 // TODO(henrika): turn off BT if required. | 511 // TODO(henrika): turn off BT if required. |
| 552 mAudioDeviceState = STATE_WIRED_HEADSET_ON; | 512 mActiveAudioDevice = DEVICE_WIRED_HEADSET; |
| 553 setSpeakerphoneOn(false); | 513 setSpeakerphoneOn(false); |
| 554 break; | 514 break; |
| 555 case DEVICE_EARPIECE: | 515 case DEVICE_EARPIECE: |
| 556 // TODO(henrika): turn off BT if required. | 516 // TODO(henrika): turn off BT if required. |
| 557 mAudioDeviceState = STATE_EARPIECE_ON; | 517 mActiveAudioDevice = DEVICE_EARPIECE; |
| 558 setSpeakerphoneOn(false); | 518 setSpeakerphoneOn(false); |
| 559 break; | 519 break; |
| 560 default: | 520 default: |
| 561 loge("Invalid audio device selection!"); | 521 loge("Invalid audio device selection!"); |
| 562 break; | 522 break; |
| 563 } | 523 } |
| 564 reportUpdate(); | 524 reportUpdate(); |
| 565 } | 525 } |
| 566 | 526 |
| 527 private static int selectDefaultDevice(boolean[] devices) { | |
| 528 // Use a special selection scheme if the default device is selected. | |
| 529 // The "most unique" device will be selected; Wired headset first, | |
| 530 // then Bluetooth and last the speaker phone. | |
| 531 if (devices[DEVICE_WIRED_HEADSET]) { | |
| 532 return DEVICE_WIRED_HEADSET; | |
| 533 } else if (devices[DEVICE_BLUETOOTH_HEADSET]) { | |
| 534 // TODO(henrika): possibly need improvements here if we are | |
| 535 // in a state where Bluetooth is turning off. | |
| 536 return DEVICE_BLUETOOTH_HEADSET; | |
| 537 } | |
| 538 return DEVICE_SPEAKERPHONE; | |
| 539 } | |
| 540 | |
| 567 private int getNumOfAudioDevicesWithLock() { | 541 private int getNumOfAudioDevicesWithLock() { |
| 568 int count = 0; | 542 int count = 0; |
| 569 for (int i = 0; i < DEVICE_COUNT; ++i) { | 543 for (int i = 0; i < DEVICE_COUNT; ++i) { |
| 570 if (mAudioDevices[i]) | 544 if (mAudioDevices[i]) |
| 571 count++; | 545 count++; |
| 572 } | 546 } |
| 573 return count; | 547 return count; |
| 574 } | 548 } |
| 575 | 549 |
| 576 /** | 550 /** |
| 551 * Updates the active device given the current list of devices and | |
| 552 * information about if a specific device has been selected or if | |
| 553 * the default device is selected. | |
| 554 */ | |
| 555 private void updateDeviceActivation() { | |
| 556 boolean devices[] = null; | |
| 557 int selected = DEVICE_INVALID; | |
| 558 synchronized (mLock) { | |
| 559 selected = mSelectedAudioDevice; | |
| 560 devices = mAudioDevices.clone(); | |
| 561 } | |
| 562 if (selected == DEVICE_INVALID) { | |
| 563 loge("Unable to activate device since no device is selected!"); | |
| 564 return; | |
| 565 } | |
| 566 | |
| 567 // Update default device if it has been selected explicitly, or | |
| 568 // the selected device has been removed from the list. | |
| 569 if (selected == DEVICE_DEFAULT || !devices[mSelectedAudioDevice]) { | |
| 570 // Get default device given current list and activate the device. | |
| 571 int defaultDevice = selectDefaultDevice(devices); | |
| 572 setAudioDevice(defaultDevice); | |
| 573 } else { | |
| 574 // Activate the selected device since we know that it exists in | |
| 575 // the list. | |
| 576 setAudioDevice(selected); | |
| 577 } | |
| 578 } | |
| 579 | |
| 580 private static boolean isNumeric(String str) { | |
| 581 for (char c : str.toCharArray()) { | |
| 582 if (!Character.isDigit(c)) { | |
| 583 return false; | |
| 584 } | |
| 585 } | |
| 586 return true; | |
| 587 } | |
| 588 | |
| 589 /** | |
| 577 * For now, just log the state change but the idea is that we should | 590 * For now, just log the state change but the idea is that we should |
| 578 * notify a registered state change listener (if any) that there has | 591 * notify a registered state change listener (if any) that there has |
| 579 * been a change in the state. | 592 * been a change in the state. |
| 580 * TODO(henrika): add support for state change listener. | 593 * TODO(henrika): add support for state change listener. |
| 581 */ | 594 */ |
| 582 private void reportUpdate() { | 595 private void reportUpdate() { |
| 583 synchronized (mLock) { | 596 synchronized (mLock) { |
| 584 List<String> devices = new ArrayList<String>(); | 597 List<String> devices = new ArrayList<String>(); |
| 585 for (int i = 0; i < DEVICE_COUNT; ++i) { | 598 for (int i = 0; i < DEVICE_COUNT; ++i) { |
| 586 if (mAudioDevices[i]) | 599 if (mAudioDevices[i]) |
| 587 devices.add(DEVICE_NAMES[i]); | 600 devices.add(DEVICE_NAMES[i]); |
| 588 } | 601 } |
| 589 logd("reportUpdate: state=" + mAudioDeviceState | 602 if (DEBUG) { |
| 590 + ", devices=" + devices); | 603 logd("reportUpdate: active=" + mActiveAudioDevice |
| 604 + ", selected=" + mSelectedAudioDevice | |
| 605 + ", devices=" + devices); | |
| 606 } | |
| 591 } | 607 } |
| 592 } | 608 } |
| 593 | 609 |
| 594 private void logDeviceInfo() { | 610 private void logDeviceInfo() { |
| 595 Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + | 611 Log.i(TAG, "Manufacturer:" + Build.MANUFACTURER + |
| 596 " Board: " + Build.BOARD + " Device: " + Build.DEVICE + | 612 " Board: " + Build.BOARD + " Device: " + Build.DEVICE + |
| 597 " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); | 613 " Model: " + Build.MODEL + " PRODUCT: " + Build.PRODUCT); |
| 598 } | 614 } |
| 599 | 615 |
| 600 /** Trivial helper method for debug logging */ | 616 /** Trivial helper method for debug logging */ |
| 601 private void logd(String msg) { | 617 private void logd(String msg) { |
| 602 Log.d(TAG, msg); | 618 Log.d(TAG, msg); |
| 603 } | 619 } |
| 604 | 620 |
| 605 /** Trivial helper method for error logging */ | 621 /** Trivial helper method for error logging */ |
| 606 private void loge(String msg) { | 622 private void loge(String msg) { |
| 607 Log.e(TAG, msg); | 623 Log.e(TAG, msg); |
| 608 } | 624 } |
| 609 | 625 |
| 626 /** What a Terrible Failure: Reports a condition that should never happen */ | |
| 627 private void logwtf(String msg) { | |
| 628 Log.wtf(TAG, msg); | |
| 629 } | |
| 630 | |
| 610 private class SettingsObserver extends ContentObserver { | 631 private class SettingsObserver extends ContentObserver { |
| 611 SettingsObserver() { | 632 SettingsObserver() { |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
change the constructor to be:
SettingsObserver(Ha
| |
| 612 super(new Handler()); | 633 super(new Handler()); |
| 613 mContentResolver.registerContentObserver(Settings.System.CONTENT_URI , true, this); | 634 mContentResolver.registerContentObserver(Settings.System.CONTENT_URI , true, this); |
| 614 } | 635 } |
| 615 | 636 |
| 616 @Override | 637 @Override |
| 617 public void onChange(boolean selfChange) { | 638 public void onChange(boolean selfChange) { |
| 618 super.onChange(selfChange); | 639 super.onChange(selfChange); |
| 619 int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE _CALL); | 640 int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE _CALL); |
| 620 nativeSetMute(mNativeAudioManagerAndroid, (volume == 0)); | 641 nativeSetMute(mNativeAudioManagerAndroid, (volume == 0)); |
| 621 } | 642 } |
| 622 } | 643 } |
| 623 | 644 |
| 624 private native void nativeSetMute(long nativeAudioManagerAndroid, boolean mu ted); | 645 private native void nativeSetMute(long nativeAudioManagerAndroid, boolean mu ted); |
| 625 | 646 |
| 626 private class SettingsObserverThread extends Thread { | 647 private class SettingsObserverThread extends Thread { |
|
tommi (sloooow) - chröme
2013/12/12 14:29:45
Remove this class.
henrika (OOO until Aug 14)
2013/12/12 15:39:15
Done.
| |
| 627 SettingsObserverThread() { | 648 SettingsObserverThread() { |
| 628 super("SettinsObserver"); | 649 super("SettingsObserver"); |
| 629 } | 650 } |
| 630 | 651 |
| 631 @Override | 652 @Override |
| 632 public void run() { | 653 public void run() { |
| 633 // Set this thread up so the handler will work on it. | 654 // Set this thread up so the handler will work on it. |
| 634 Looper.prepare(); | 655 Looper.prepare(); |
| 635 | 656 |
| 636 synchronized (mSettingsObserverLock) { | 657 synchronized (mSettingsObserverLock) { |
| 637 mSettingsObserver = new SettingsObserver(); | 658 mSettingsObserver = new SettingsObserver(); |
| 638 mSettingsObserverLock.notify(); | 659 mSettingsObserverLock.notify(); |
| 639 } | 660 } |
| 640 | 661 |
| 641 // Listen for volume change. | 662 // Listen for volume change. |
| 642 Looper.loop(); | 663 Looper.loop(); |
| 643 } | 664 } |
| 644 } | 665 } |
| 645 } | 666 } |
| OLD | NEW |