我正在尝试在 Android 中构建一个音乐应用程序,尽管有时我觉得自己是个可笑的新手。我进行了很多研究,尽我所能了解 MediaPlayer、MediaBrowserCompat / MediaBrowserServiceCompat、MediaController 和 Services 的工作原理,以及数十篇关于如何构建一个不幸的旧教程。
我最大的问题是他们中的大多数人倾向于使用 IBinder 功能和意图来绑定和启动 musicPlaybackService,而 google 的文档使用这些 MediaBrowser 和 MediaBrowserService API,这两种方法都是新的,老实说对我来说非常困难和压倒性的。
到目前为止,我学到了很多东西,但很难。我发现的两个稍微好一点的教程是 https://www.sitepoint.com/a-step-by-step-guide-to-building-an-android-audio-player-app/和https:// code.tutsplus.com/tutorials/background-audio-in-android-with-mediasessioncompat--cms-27030他们使用第一种和第二种方法。我的应用程序版本是我通过将我学到的所有部分拼凑而成的。
我自己设法解决了很多错误和问题,但我遇到了一个 NullPointer 异常,我根本不知道如何解决。这个调试器也很奇怪,感觉就像每次错误都来自程序的另一个地方;有时它会在我放置的断点处停止,然后使用完全相同的代码和断点再次运行调试器,它会跳过它们并直接进入运行时错误。
这是我的 Manifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ecebuc.gesmediaplayer">
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MediaPlaybackService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.AUDIO_BECOMING_NOISY" />
</intent-filter>
</receiver>
</application>
然后这是我的 MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private final int REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE = 101;
private static final int STATE_PAUSED = 0;
private static final int STATE_PLAYING = 1;
private static int currentState;
private MediaBrowserCompat gesMediaBrowser;
private MediaControllerCompat gesMediaController;
public MediaControllerCompat.TransportControls gesPlaybackController;
private Button playPauseToggleButton;
private MediaBrowserCompat.ConnectionCallback mediaBrowserCallbacks = new MediaBrowserCompat.ConnectionCallback() {
@Override
public void onConnected() {
super.onConnected();
try {
//create the media controller and register the callbacks to stay in sync
gesMediaController = new MediaControllerCompat(MainActivity.this, gesMediaBrowser.getSessionToken());
gesMediaController.registerCallback(mediaControllerCallbacks);
//save the controller and define the easy access transport controls in the object
MediaControllerCompat.setMediaController(MainActivity.this, gesMediaController);
gesPlaybackController = gesMediaController.getTransportControls();
//Display initial state
MediaMetadataCompat metadata = gesMediaController.getMetadata();
PlaybackStateCompat pbState = gesMediaController.getPlaybackState();
} catch( RemoteException e ) {
}
}
@Override
public void onConnectionSuspended() {
// The Service has crashed. Disable transport controls until it automatically reconnects
}
@Override
public void onConnectionFailed() {
// The Service has refused our connection
Log.d("onConnectionFail: ", "the service hasn't been able to connect");
}
};
private MediaControllerCompat.Callback mediaControllerCallbacks = new MediaControllerCompat.Callback() {
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
super.onMetadataChanged(metadata);
}
@Override
public void onPlaybackStateChanged(PlaybackStateCompat state) {
super.onPlaybackStateChanged(state);
if( state == null ) {
Log.d("onPlaybackChange: ", "the state is null");
Toast.makeText(MainActivity.this,
"onPlaybackStateChange: the state is null",
Toast.LENGTH_SHORT)
.show();
return;
}
switch( state.getState() ) {
case PlaybackStateCompat.STATE_PLAYING: {
currentState = STATE_PLAYING;
break;
}
case PlaybackStateCompat.STATE_PAUSED: {
currentState = STATE_PAUSED;
break;
}
}
}
@Override
public void onSessionDestroyed(){
// Override to handle the session being destroyed
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//grab the buttons for media playback control
playPauseToggleButton = (Button)findViewById(R.id.playPause_btn);
//request permissions for external storage
if (ContextCompat.checkSelfPermission(this,
android.Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// Permission have not been granted
ActivityCompat.requestPermissions(this,
new String[]{android.Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE);
}
else{
//permissions have already been granted
}
//initiate connection to the MediaPlaybackService through MediaBrowser
gesMediaBrowser = new MediaBrowserCompat(this,
new ComponentName(this, MediaPlaybackService.class),
mediaBrowserCallbacks, getIntent().getExtras());
gesMediaBrowser.connect();
//Attach listeners to them
playPauseToggleButton.setOnClickListener(this);
// space here for other buttons
// sapce here for other buttons
}
@Override
protected void onStart() {
super.onStart();
//gesMediaBrowser.connect();
}
/*protected void onStop() {
super.onStop();
// (see "stay in sync with the MediaSession")
if( gesMediaController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
gesPlaybackController.pause();
}
gesMediaBrowser.disconnect();
}*/
@Override
protected void onDestroy() {
super.onDestroy();
/*if (gesMediaController != null) {
gesMediaController.unregisterCallback(mediaControllerCallbacks);
gesMediaController = null;
}
if(gesMediaBrowser != null && gesMediaBrowser.isConnected()) {
gesMediaBrowser.disconnect();
gesMediaBrowser = null;
}*/
if( gesMediaController.getPlaybackState().getState() == PlaybackStateCompat.STATE_PLAYING ) {
gesPlaybackController.pause();
}
gesMediaBrowser.disconnect();
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.playPause_btn:
//has to be dealt with accordingly, based on the current state of mediaplayer
int currentState = gesMediaController.getPlaybackState().getState();
if(currentState == PlaybackStateCompat.STATE_PLAYING) {
gesPlaybackController.pause();
} else {
//gesPlaybackController.play();
gesPlaybackController.playFromMediaId(String.valueOf(R.raw.warner_tautz_off_broadway), null);
}
break;
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_GESPLAYER_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "onRequestPermissionResult: granted", Toast.LENGTH_SHORT).show();
} else {
//close the app if permissions aren't granted
//Toast.makeText(this, "onRequestPermissionResult: denied", Toast.LENGTH_SHORT).show();
finish();
}
return;
}
// other 'case' lines to check for other
// permissions this app might request.
}
}
然后是playbackService.java
public class MediaPlaybackService extends MediaBrowserServiceCompat implements
MediaPlayer.OnCompletionListener,
AudioManager.OnAudioFocusChangeListener {
public static final String COMMAND_EXAMPLE = "command_example";
public static boolean isServiceStarted = false;
/*public int audioIndex;
public ArrayList<Audio> audioList;
public Audio activeAudio;*/
private MediaPlayer gesMediaPlayer;
private MediaSessionCompat gesMediaSession;
private int pausedPosition;
//-------------------------------------Lifecycle methods--------------------------------------//
@Override
public void onCreate() {
super.onCreate();
Log.d("onCreate: ", "Service created");
initMediaPlayer();
initMediaSession();
callStateListener();
registerNoisyReceiver();
// Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player
PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY |
PlaybackStateCompat.ACTION_PLAY_PAUSE);
gesMediaSession.setPlaybackState(playbackStateBuilder.build());
}
@Override
public void onDestroy() {
super.onDestroy();
if (gesMediaPlayer != null) {
gesMediaPlayer.stop();
gesMediaPlayer.release();
}
//Disable the PhoneStateListener
if (phoneStateListener != null) {
telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
}
removeAudioFocus();
unregisterReceiver(becomingNoisyReceiver);
//clear cached playlist
//new StorageUtils(getApplicationContext()).clearCachedAudioPlaylist();
NotificationManagerCompat.from(this).cancel(1);
stopSelf();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("onStartCommand: ", "Service has been started");
MediaButtonReceiver.handleIntent(gesMediaSession, intent);
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
if( gesMediaPlayer != null ) {
gesMediaPlayer.release();
}
}
//----------------------------------------Initialisers----------------------------------------//
private void initMediaPlayer() {
gesMediaPlayer = new MediaPlayer();
gesMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
gesMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
gesMediaPlayer.setVolume(1.0f, 1.0f);
/*try {
//sets songPath as data source for media player
//gesMediaPlayer.setDataSource(songPath);
//sets current song as data source for media player
gesMediaPlayer.setDataSource(activeAudio.getData());
} catch (IOException e) {
e.printStackTrace();
stopSelf();
}
gesMediaPlayer.prepareAsync();*/
}
private void initMediaSession() {
ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
gesMediaSession = new MediaSessionCompat(getApplicationContext(), "GESMediaService",
mediaButtonReceiver, null);
gesMediaSession.setCallback(mediaSessionCallbacks);
gesMediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS );
//this is for pre-Lollipop media button handling on those devices
Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
mediaButtonIntent.setClass(this, MediaButtonReceiver.class);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
gesMediaSession.setMediaButtonReceiver(pendingIntent);
// Set the session's token so that client activities can communicate with it.
setSessionToken(gesMediaSession.getSessionToken());
}
private void registerNoisyReceiver() {
//Handles headphones coming unplugged. cannot be done through a manifest receiver
IntentFilter noisyFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
registerReceiver(becomingNoisyReceiver, noisyFilter);
}
private void initMediaSessionMetadata() {
MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
//Notification icon in card
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
//lock screen icon for pre lollipop
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, "Display Title");
metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, "Display Subtitle");
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, 1);
metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, 1);
gesMediaSession.setMetadata(metadataBuilder.build());
}
//-----------------------------------Media Playback functions---------------------------------//
//TODO: read about the AssetFileDescriptor, and the ResultReceiver
private MediaSessionCompat.Callback mediaSessionCallbacks = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
super.onPlay();
if(!requestAudioFocus()) {
//failed to gain focus
return;
}
//check if service is started, not only bound
if(!isServiceStarted){
startService(new Intent(getApplicationContext(), MediaPlaybackService.class));
}
gesMediaSession.setActive(true);
setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);
showPlayingNotification();
gesMediaPlayer.start();
}
@Override
public void onPause() {
super.onPause();
if( gesMediaPlayer.isPlaying() ) {
gesMediaPlayer.pause();
setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED);
showPausedNotification();
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
super.onPlayFromMediaId(mediaId, extras);
try {
AssetFileDescriptor afd = getResources().openRawResourceFd(Integer.valueOf(mediaId));
if( afd == null ) {
Log.d("afd: ", "afd in onPlayFromMediaId is null");
return;
}
try {
gesMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
} catch( IllegalStateException e ) {
gesMediaPlayer.release();
initMediaPlayer();
gesMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
}
afd.close();
initMediaSessionMetadata();
} catch (IOException e) {
e.printStackTrace();
return;
}
try {
gesMediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
Log.d("onPlayFromId: ", "mediaPlayer failed to prepare");
}
//Work with extras here if you want
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb) {
super.onCommand(command, extras, cb);
if( COMMAND_EXAMPLE.equalsIgnoreCase(command) ) {
//Custom command here
}
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
};
private void setMediaPlaybackState(int state) {
PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder();
if( state == PlaybackStateCompat.STATE_PLAYING ) {
playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_PAUSE);
} else {
playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE |
PlaybackStateCompat.ACTION_PLAY);
}
playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0);
gesMediaSession.setPlaybackState(playbackstateBuilder.build());
}
//-------------------------------Audio Focus and Calls Handling-------------------------------//
//Handle incoming phone calls
private boolean ongoingCall = false;
private PhoneStateListener phoneStateListener;
private TelephonyManager telephonyManager;
private AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
@Override
public void onAudioFocusChange(int focusChange) {
switch( focusChange ) {
case AudioManager.AUDIOFOCUS_LOSS: {
// Lost focus for an unbounded amount of time:
// stop playback and release media player
if( gesMediaPlayer.isPlaying() ) {
gesMediaPlayer.stop();
}
/*gesMediaPlayer.release();
gesMediaPlayer = null;*/
break;
}
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: {
// Lost focus for a short time; Pause only and do not
// release the media player as playback is likely to resume
if (gesMediaPlayer.isPlaying()) {
gesMediaPlayer.pause();
}
break;
}
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
// Lost focus for a short time (ex. notification sound)
// but it's ok to keep playing at a temporarily attenuated level
if( gesMediaPlayer != null ) {
gesMediaPlayer.setVolume(0.2f, 0.2f);
}
break;
}
case AudioManager.AUDIOFOCUS_GAIN: {
//Invoked when the audio focus of the system is updated.
if( gesMediaPlayer != null ) {
if( !gesMediaPlayer.isPlaying() ) {
gesMediaPlayer.start();
}
gesMediaPlayer.setVolume(1.0f, 1.0f);
}
break;
}
}
}
private boolean requestAudioFocus() {
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_GAIN;
}
private boolean removeAudioFocus() {
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
audioManager.abandonAudioFocus(this);
}
private void callStateListener() {
// Get the telephony manager
telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
//Starting listening for PhoneState changes
phoneStateListener = new PhoneStateListener() {
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
//if at least one call exists or the phone is ringing
//pause the MediaPlayer
case TelephonyManager.CALL_STATE_OFFHOOK:
case TelephonyManager.CALL_STATE_RINGING:
if (gesMediaPlayer != null && gesMediaPlayer.isPlaying()) {
gesMediaPlayer.pause();
pausedPosition = gesMediaPlayer.getCurrentPosition();
ongoingCall = true;
}
break;
case TelephonyManager.CALL_STATE_IDLE:
// Phone idle. Start/resume playing.
if (gesMediaPlayer != null) {
if (ongoingCall) {
ongoingCall = false;
gesMediaPlayer.seekTo(pausedPosition);
gesMediaPlayer.start();
}
}
break;
}
}
};
// Register the listener with the telephony manager
// Listen for changes to the device call state.
telephonyManager.listen(phoneStateListener,
PhoneStateListener.LISTEN_CALL_STATE);
}
private BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if( gesMediaPlayer != null && gesMediaPlayer.isPlaying() ) {
gesMediaPlayer.pause();
}
}
};
//------------------------------------Less important methods----------------------------------//
@Nullable
@Override
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
if(TextUtils.equals(clientPackageName, getPackageName())) {
return new BrowserRoot(getString(R.string.app_name), null);
}
return null;
}
//Not important for general audio service, required for class
@Override
public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
result.sendResult(null);
}
}
我为整个代码道歉,但在这一点上,我不知道我做错了什么或去哪里看。特别是因为该应用程序确实在以前的版本中工作。如果你们在我的代码中看到任何具体的内容,任何建议都将不胜感激。我尝试在可能的地方捕获异常,但同时我不确定应该将 try-catch 结构放在哪里。我正在努力学习 谢谢大家!