[Android] A Simple Local Music Player

Preface

Github: Demo Download
The main functions are as follows:
1. Desktop Widget, which can switch songs, pause/start playing, display song title and singer name
 2. When switching songs, there is a system bar prompt to quickly navigate back to the playback interface.
3. Search for music files in SD card and add them to ListView
 4. SeekBar drag can control the progress of music playing
 5. Playing songs can be marked in ListView
 6. Play/pause and cut songs in playback interface
General idea:
For music data storage:

	We can use Cursor to take out the music title, singer name, playback time, storage address and album chart information of SD card.
Write to Android's SQLite database, where we need to use the above music file information, and hold the location information of the music directly.
Find the corresponding music files in the database.

For database establishment:

		We need an object to build a database and a method class to operate on the database object, so that we can add or delete the database.
	Implementing multiple Activities to share a database object. In our database, we need to store the specific information of the music files searched in SD cards.
	The music information of body also needs us to provide a specific JavaBean class. So every time we look up music data, we can return one that contains all of us.
	JavaBean for required music information.
	
Regarding services:

		For music players, we first need a music playback service that accepts widgets from our list interface, playback interface and desktop widgets.
	Play action, music play service can convert these actions into a specific location information, and then take the location information to the database to check the song.
	Store address, complete playback;
	
		For system message prompts, we also need a message prompt class. Android provides us with a Notification Manager to facilitate our tuning.
	With the system's message prompting service, we need to send the music file information currently playing to Notification, and then through Notification.-
	Manager is OK when it is released!

About the database section

First, we need to establish what is stored in our database:
Music address----------------------------------------------------------------------------------------------------------------------------------------------------
Music Name - -------------------------------------- Music List, Desktop Widget, Playback Interface, System Tips... Used for display
Singer - ----------------------------------------------- Ibid.
Music Picture Address - --------------------------------------------------- Play Interface for Display
Duration - ---------------------------------------- Our slider needs to show time and adjust progress.
A self-increasing key - --------------------------------------------------------------------------------------------------------------------------------------

According to the above requirements, we can write a JavaBean, which is a music file information class.

//Contents of Music Files
public MusicItem(String title, String artist, String path, long duration, String imagePath) {

        this.artist = artist;
        this.duration = duration;
        this.path = path;
        this.title = title;
        this.imagePath = imagePath;
    }
//Get album pictures of music according to the address of the music file
 public Bitmap loadPicture(String path){

        MediaMetadataRetriever mediaMetadataRetriever=new MediaMetadataRetriever();
        mediaMetadataRetriever.setDataSource(path);
        byte[] picture = mediaMetadataRetriever.getEmbeddedPicture();
        Bitmap bitmap= BitmapFactory.decodeByteArray(picture,0,picture.length);
        return bitmap;
    }

There's nothing to say about the establishment of the database. The column names correspond to our JavaBean s.

 db.execSQL("create table my_music (" +
                "id integer  primary key autoincrement," +
                "title text," +
                "artist text, " +
                "path text, " +
                "duration long, " +
                "imagePath text)");
    }

With the database object, we need to add and delete the operation of this object, establish a database operation class, add and delete three methods of database:

//increase
    public void add(MusicItem music){

        db = helper.getWritableDatabase();
        db.execSQL("insert into my_music (title, artist, path, duration, imagePath) values (?, ?, ?, ?, ?)",
                new Object[]{music.getTitle(), music.getArtist(), music.getPath(), music.getDuration(), music.getImagePath()});
    }


//Delete
public void delete(){

        db = helper.getWritableDatabase();
        db.delete("my_music", null, null);
        db.execSQL("update sqlite_sequence set seq=0 where name='my_music'" );
    }


//check
public MusicItem find(int id){

        db = helper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from my_music where id = ?", new String[]{
                String.valueOf(id)
        });
        if (cursor.moveToNext()){
            return new MusicItem(cursor.getString(cursor.getColumnIndex("title")),
                    cursor.getString(cursor.getColumnIndex("artist")),
                    cursor.getString(cursor.getColumnIndex("path")),
                    cursor.getLong(cursor.getColumnIndex("duration")),
                    cursor.getString(cursor.getColumnIndex("imagePath")));
        }
        return null;
    }


//Self-incrementing key to get the last record in the database
public int getCount(){

        db = helper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select count(id) from my_music", null);
        if (cursor.moveToNext()){
            return cursor.getInt(0);
        }
        return 0;
    }

Information about music files can be obtained through cursor. getColumn Index (MediaStore. Audio. Media. * * * *) provided by Android.
The specific operation code is as follows:

    //First determine that the search object is the music file of the external memory card
    Cursor cursor = context.getContentResolver()
            .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    null,
                    null,
                    null,
                    MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
		//Classification of Searched Music File Information
        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
        int pathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
        int durationIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
        int imagePathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);

        while (cursor.moveToNext()) {

            String Path = cursor.getString(pathIndex);
            String Title = cursor.getString(titleIndex);
            String Artist = cursor.getString(artistIndex);
            long Duration = cursor.getLong(durationIndex);
            String imagePath = cursor.getString(imagePathIndex);
            MusicItem song = new MusicItem(Title, Artist, Path, Duration, imagePath);

			//Store in database
            musicDAO.add(song);
        }
        cursor.close();

Music List

My music list here uses ListView adapter custom BaseAdapter. Simple Adapter with Android can also be used in pursuit of minimalism. If you want to be beautiful and high performance, I recommend trying RecyclerView.

Here I choose ListView, so the old rule is to write the adapter first:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;

        if (convertView == null) {
        
            viewHolder = new ViewHolder();
            convertView = mInflater.inflate(R.layout.music_adapter, null);
            viewHolder.title = convertView.findViewById(R.id.Title);
            viewHolder.singer = convertView.findViewById(R.id.Singer);
            convertView.setTag(viewHolder);

        } else {

            viewHolder = (ViewHolder) convertView.getTag();
        }

        viewHolder.title.setText(musicItem.get(position).getTitle());
        viewHolder.singer.setText(musicItem.get(position).getArtist());

        return convertView;
    }

Here's just the getView () section. The rest is very simple. Next we apply the adapter:

		MusicAdapter adapter;
		ListView listView;
		List<MusicItem> musicList;
		... ...
		adapter = new MusicAdapter(this, musicList);
        listView = findViewById(R.id.list_view);
        listView.setAdapter(adapter);

At this point, our infrastructure is almost complete. Next is the functional implementation part of the whole program. First, logic is sorted out.
Let's click on the music list - - - - > get position - - - - - - - > pass to the music playback service (start playing) + pass to the playback interface (display time, display album pictures, display song / singer name) + pass to the desktop Widget (display song / singer name) + pass to the system message prompt service (prompt "* song start playing!" ""
Because our self-increasing primary key ID = position + 1, each interface will take this position to the database to find the corresponding song information after getting the position:

MusicDAO musicDAO = new MusicDAO(this);
musicDAO.find(position).get××××()

Here we can use global broadcasting and send position s to inform activities to update their work objects, so we need to open their services in the list interface.

	 //Broadcast to Service
     Intent intent1 = new Intent(Utils.POSITION);
     intent1.putExtra("position", position + 1);

   	 //Broadcast to note service
     Intent intent3 = new Intent("data_for_note");
     intent3.putExtra("position", position + 1);

Send a global broadcast informing each Activity:

	//Broadcast to plug-in
    Intent intent2 = new Intent(Utils.POSITION_FOR_WIDGET);
    intent2.putExtra("position", position + 1);

    //Broadcast sent back to ListView
    Intent intent4 = new Intent("position_for_list");
    intent4.putExtra("position", position + 1);
	            
	//Delivery to Player Interface by Hop Forwarding
    Intent intent = new Intent(context, PlayActivity.class);
    intent.putExtra("position", position + 1);

Music Playing Interface

First, we accept the position from the previous list interface and update the UI:

	imageView.setImageBitmap(musicDAO.find(position).loadPicture(musicDAO.find(position).getPath()));
    totalTime.setText(simpleDateFormat.format(musicDAO.find(position).getDuration()));
    tv_Title.setText(musicDAO.find(position).getTitle());
    tv_Singer.setText(musicDAO.find(position).getArtist());
    seekBar.setMax((int) musicDAO.find(position).getDuration());
On Progress Bar

The current playback progress can be obtained from the mediaPlayer.getCurrentPosition() method in the service. We only need to call the getCurrentPosition() method in the service to get the current playback progress by binding the binder object returned from the service:

//Get the returned binder object
 @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        binder = (MusicService.Binder) service;
    }
//Update UI
Handler handler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             super.handleMessage(msg);
             if (msg.what == 1) {
                 if (binder != null) {
                     seekBar.setProgress(binder.getProgress());
                     nowTime.setText(simpleDateFormat.format(binder.getProgress()));
                 }
             }
             Message message = Message.obtain();
             message.what = 1;
             handler.sendMessageDelayed(message, 500);
         }
     };

For us to drag the progress bar and adjust the music playback progress, we send a program broadcast to Service by onProgressChanged ():

@Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        nowTime.setText(simpleDateFormat.format(progress));
        if (fromUser){
            Intent intent = new Intent(Utils.SEEKBAR);
            intent.putExtra("progress", progress);
            sendBroadcast(intent);
        }
    }
Update UI

Next, it's very simple. Click on the switch song to send an action broadcast to Music Service:

case R.id.Next_Button:
                intent.putExtra("action", Utils.NEXT_ACTION);
                start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                status = 1;
                position ++;
                sendBroadcast(intent);
                break;

            case R.id.pre_Button:
                intent.putExtra("action", Utils.PRE_ACTION);
                start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                status = 1;
                position --;
                sendBroadcast(intent);
                break;

            case R.id.play_or_stop:
                intent.putExtra("action", Utils.START_OR_STOP);
                if (status == 0){
                    start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                    status = 1;
                } else if (status == 1){
                    start_or_stop_view.setBackgroundResource(R.mipmap.ic_play);
                    status = 0;
                }
                sendBroadcast(intent);
                break;

            default:
                break;
        }
    }

Update the UI of the first and the last one. Here we need to pay attention to whether it is the first or the last one in the database. The code is mainly placed in the SetView() method:?
Intent intent = new Intent(Utils.POSITION_FOR_WIDGET);

    if (position == 0) {

        imageView.setAlpha(0.5f);
    	imageView.setImageBitmap(musicDAO.find(musicDAO.getCount()).loadPicture(musicDAO.find(musicDAO.getCount()).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(musicDAO.getCount()).getDuration()));
        tv_Title.setText(musicDAO.find(musicDAO.getCount()).getTitle());
        tv_Singer.setText(musicDAO.find(musicDAO.getCount()).getArtist());
        seekBar.setMax((int) musicDAO.find(musicDAO.getCount()).getDuration());
        position = musicDAO.getCount();
        
    } else if (position > musicDAO.getCount()) {

        imageView.setAlpha(0.7f);
        imageView.setImageBitmap(musicDAO.find(1).loadPicture(musicDAO.find(1).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(1).getDuration()));
        tv_Title.setText(musicDAO.find(1).getTitle());
        tv_Singer.setText(musicDAO.find(1).getArtist());
        seekBar.setMax((int) musicDAO.find(1).getDuration());
        position = 1;
        
    } else {
    
        imageView.setAlpha(0.5f);
        imageView.setImageBitmap(musicDAO.find(position).loadPicture(musicDAO.find(position).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(position).getDuration()));
        tv_Title.setText(musicDAO.find(position).getTitle());
        tv_Singer.setText(musicDAO.find(position).getArtist());
        seekBar.setMax((int) musicDAO.find(position).getDuration());
    }
}

Music Service

As above, let's sort out the logic first.

Now that you want to receive messages, let's start with a Broadcast Receiver:
Registered listener:

		IntentFilter filter = new IntentFilter();
        filter.addAction(Utils.POSITION);
        filter.addAction(Utils.CONTROL_ACTION);
        filter.addAction("data_from_widget");
        filter.addAction("pop_action");
        filter.addAction(Utils.SEEKBAR);
        registerReceiver(myReceiver, filter);

The realization of internal logic:

public class MyReceiver extends BroadcastReceiver {

        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        @Override
        public void onReceive(Context context, Intent intent) {
            nowPlay = intent.getIntExtra("position", 0);
            String pop = intent.getStringExtra("pop_action");
            action = intent.getStringExtra("action");
            int progress = intent.getIntExtra("progress", -1);
           if (nowPlay !=  0 ){
               position = nowPlay;
               prepareMusic(position);
           } else if (pop != null){
               if (mediaPlayer.isPlaying()){
                   mediaPlayer.pause();
               } else {
                   mediaPlayer.start();
               }
           }else if (action != null){
               switch (Objects.requireNonNull(intent.getAction())){
                   case Utils.CONTROL_ACTION:
                       if (action.equals(Utils.NEXT_ACTION)){
                           next();
                       } else if (action.equals(Utils.PRE_ACTION)){
                           pre();
                       } else if (action.equals(Utils.START_OR_STOP)){
                           if (mediaPlayer.isPlaying()){
                               mediaPlayer.pause();
                           } else {
                               mediaPlayer.start();
                           }
                       }
                       break;

                   default:
                       break;
               }
           } else if (progress != -1){
               mediaPlayer.seekTo(progress);
           }
        }
    }

Specific implementation code of playing music:
private void prepareMusic(final int position) {

    try {
        mediaPlayer.reset();
        mediaPlayer.setDataSource(musicDAO.find(position).getPath());
        mediaPlayer.prepare();
        mediaPlayer.start();
        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer arg0) {
                Intent intent = new Intent(Utils.FINISH);
                intent.putExtra("finish", Utils.FINISH);
                sendBroadcast(intent);
                next();//If the current song is played out, the next song will be played automatically.
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

The first one:

private void pre(){
        if (position == 1){
            prepareMusic(musicDAO.getCount());
            position = musicDAO.getCount();
        } else {
            prepareMusic(--position);
        }
    }

Next:

private void next(){
        if (position == musicDAO.getCount()){
            prepareMusic(1);

            position = 1;
        } else {
            prepareMusic(++ position);

        }
    }

Notification Service

This logic is very simple, accepting broadcasts from services, Listview, and setting Title:

BroadcastReceiver receiver = new BroadcastReceiver() {
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void onReceive(Context context, Intent intent) {
            if (musicDAO == null){
                musicDAO = new MusicDAO(context);
            }
            position = intent.getIntExtra("position", -1);
            if (position == 0){
                Log.e("Push", "No push");
            } else {

                Intent intent1 = new Intent(getApplicationContext(), PlayActivity.class);
                intent1.putExtra("position", position);
                messageNotification = new Notification.Builder(context)
                        .setContentTitle("New mail from MediaPlayer")
                        .setContentText(musicDAO.find(position).getTitle() + "It's starting to play.")
                        .setSmallIcon(R.mipmap.music)
                        .setAutoCancel(true)
                        .setDefaults(Notification.DEFAULT_SOUND)
                        .setContentIntent(PendingIntent.getActivity(context,0, intent1, FLAG_UPDATE_CURRENT))
                        .build();

                // Notice Bar Message
                int messageNotificationID = 1;
                messageNotificationManager.notify(messageNotificationID, messageNotification);
            }
        }
    };

Click on the system prompt to return to the playback interface:

        Intent messageIntent = new Intent(getApplicationContext(), PlayActivity.class);
        messagePendingIntent = PendingIntent.getActivity(this, 0, messageIntent, PendingIntent.FLAG_CANCEL_CURRENT);

Overall optimization

Database section

We can put write time-consuming operations into asynchronous tasks, give users a boot prompt, reduce the onCreate burden of ListView, and also accelerate the start-up speed of App:

We encapsulate the method of writing to the database:

 public List<MusicItem> getBaseData() {

        musicList = new ArrayList<>();
        musicDAO = new MusicDAO(context);

        //First determine that the search object is the music file of the external memory card
        Cursor cursor = context.getContentResolver()
                .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                        null,
                        null,
                        null,
                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);


        assert cursor != null;

        //Classification of Searched Music File Information
        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
        int pathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
        int durationIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
        int imagePathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);

        while (cursor.moveToNext()) {

            String Path = cursor.getString(pathIndex);
            String Title = cursor.getString(titleIndex);
            String Artist = cursor.getString(artistIndex);
            long Duration = cursor.getLong(durationIndex);
            String imagePath = cursor.getString(imagePathIndex);
            MusicItem song = new MusicItem(Title, Artist, Path, Duration, imagePath);

            musicDAO.add(song);
            musicList.add(song);

        }
        cursor.close();
        return musicList;
    }

Use asynchronous tasks to accomplish it:

 @Override
    protected Void doInBackground(Void...voids) {

        musicList = getBaseData();
        return null;
    }

Before completing it, show the user a waiting prompt progress bar:

		progressDialog = new ProgressDialog(context);
        progressDialog.show();
Database Data Cleaning Section

Before using App to write data, we can empty the database information once, open the risk aversion repeatedly, and add data repeatedly:
Before writing data, add a sentence:

        musicDAO.delete();
        musicList = new ArrayList<>();
        musicDAO = new MusicDAO(context);
        ... .... .... ...

Real-time update of database data

Every time the list interface starts from the background, we can refresh the list to prevent the loss of song files or add an exception:

How to refresh the list:

 public void changeList(){
        musicDAO.delete();
        musicList = myAsyncTask.getBaseData();
        adapter = new MusicAdapter(this, musicList);
        listView.setAdapter(adapter);

        if (nowposition >= 0 && nowposition <= musicList.size() + 1){
            adapter.updateRed(nowposition - 1, listView);
        }
    }

Activity's life cycle of regaining focus from the background is in onRestart(), so we write the above method in onRestart():

   @Override
    protected void onRestart() {
        super.onRestart();
        changeList();
    }
Music List Marker

In the music Tittle TextView that is playing, we add "Playing..." "Words, get TextView through ViewHolder, call our updated TextView method, refresh the list once:

public void updateRed(int index, ListView listview){

        int visibleFirstPosition = listview.getFirstVisiblePosition();
        int visibleLastPosition = listview.getLastVisiblePosition();
        if (index >= visibleFirstPosition && index <= visibleLastPosition){

            View view = listview.getChildAt(index - visibleFirstPosition);
            ViewHolder holder = (ViewHolder) view.getTag();
            String str = holder.title.getText().toString();
            holder.title.setText("Playing now..." + str);
            musicItem.get(index).setTitle("Playing now..." + str);

        } else {

            String str = musicItem.get(index).getTitle();
            musicItem.get(index).setTitle("Playing now..." + str);
        }
    }

It should be noted that getChildAt() can only get the position of the visible part of the view, and it needs to calculate the specific location information.

Github: Demo Download

Tags: Database MediaPlayer Android github

Posted on Mon, 22 Apr 2019 16:24:33 -0700 by albertramsbottom