From 0787a71679a8de3e3ed18602b273ac92ad04609c Mon Sep 17 00:00:00 2001 From: lihongwei Date: Thu, 26 Sep 2024 18:34:20 +0800 Subject: [PATCH] =?UTF-8?q?A=E9=9D=A2=E5=89=8D=E5=8F=B0=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 29 +-- .../adapter/A_ImportFragmentAdapter.java | 100 +++++++++++ .../player/helper/CircularProgressBar.java | 17 +- .../player/javabean/A_data/AudioItem.java | 9 + .../service/MusicPlayerForegroundService.java | 166 ++++++++++++++++++ .../player/ui/activity/A_HomeActivity.java | 69 +++++--- .../player/ui/activity/A_PlayActivity.java | 139 +++++++++------ .../ui/activity/viewmodel/A_VMPlay.java | 95 ++++++---- .../player/ui/fragmnt/A_ImportFragment.java | 110 ++++++++---- .../ui/fragmnt/viewmodel/A_VMImport.java | 122 ++++++++++++- app/src/main/res/drawable/options.xml | 9 + app/src/main/res/layout/activity_ahome.xml | 3 +- app/src/main/res/layout/activity_aplay.xml | 2 +- app/src/main/res/layout/fragment_a_import.xml | 3 +- app/src/main/res/layout/item_a_home_1.xml | 2 +- app/src/main/res/layout/item_a_home_2.xml | 2 +- app/src/main/res/layout/item_a_home_3.xml | 2 +- app/src/main/res/layout/item_a_import.xml | 65 +++++++ .../main/res/mipmap-xxxhdpi/default_image.png | Bin 0 -> 27859 bytes 19 files changed, 782 insertions(+), 162 deletions(-) create mode 100644 app/src/main/java/com/hi/music/player/adapter/A_ImportFragmentAdapter.java create mode 100644 app/src/main/java/com/hi/music/player/service/MusicPlayerForegroundService.java create mode 100644 app/src/main/res/drawable/options.xml create mode 100644 app/src/main/res/layout/item_a_import.xml create mode 100644 app/src/main/res/mipmap-xxxhdpi/default_image.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 71beea1..2412e32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,11 +4,12 @@ - + - @@ -31,12 +32,12 @@ android:exported="false" /> + android:exported="false" + android:screenOrientation="portrait" /> + android:exported="true" + android:screenOrientation="portrait"> @@ -45,7 +46,7 @@ + android:exported="false"> + android:exported="true" + android:foregroundServiceType="mediaPlayback"> - + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/hi/music/player/adapter/A_ImportFragmentAdapter.java b/app/src/main/java/com/hi/music/player/adapter/A_ImportFragmentAdapter.java new file mode 100644 index 0000000..2ad1765 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/adapter/A_ImportFragmentAdapter.java @@ -0,0 +1,100 @@ +package com.hi.music.player.adapter; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.hi.music.player.R; +import com.hi.music.player.javabean.A_data.AudioItem; +import com.hi.music.player.ui.activity.A_PlayActivity; + +import java.util.ArrayList; +import java.util.List; + + +public class A_ImportFragmentAdapter extends RecyclerView.Adapter { + + private Context context; + private List audioFiles = new ArrayList<>(); + private OnDeleteClickListener onDeleteClickListener; + + public A_ImportFragmentAdapter(Context context) { + this.context = context; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_a_import, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AudioItem audioItem = audioFiles.get(position); + + holder.title.setText(audioItem.getName()); + holder.time.setText(audioItem.getDuration()); + Log.d("Adapter", "onBindViewHolder: " + audioItem.getDuration()); + + holder.deleteButton.setOnClickListener(v -> { + if (onDeleteClickListener != null) { + onDeleteClickListener.onDeleteClick(audioItem.getFile()); + } + }); + + holder.itemView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(context, A_PlayActivity.class); + intent.putExtra("Path", audioItem); + context.startActivity(intent); + } + }); + + } + + @Override + public int getItemCount() { + return audioFiles.size(); + } + + public void setAudioFiles(List audioFiles) { + this.audioFiles = audioFiles; + notifyDataSetChanged(); // Update UI when the data changes + } + + public void setOnDeleteClickListener(OnDeleteClickListener listener) { + this.onDeleteClickListener = listener; + } + + public interface OnDeleteClickListener { + void onDeleteClick(String filePath); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + ImageView deleteButton; + TextView title; + TextView time; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + + deleteButton = itemView.findViewById(R.id.options); + title = itemView.findViewById(R.id.title); + time = itemView.findViewById(R.id.time); + + } + + } +} + + diff --git a/app/src/main/java/com/hi/music/player/helper/CircularProgressBar.java b/app/src/main/java/com/hi/music/player/helper/CircularProgressBar.java index e824383..a22885e 100644 --- a/app/src/main/java/com/hi/music/player/helper/CircularProgressBar.java +++ b/app/src/main/java/com/hi/music/player/helper/CircularProgressBar.java @@ -16,6 +16,8 @@ public class CircularProgressBar extends View { private int fixedSize = 47; // 固定圆环的直径(dp) private int progressWidth = 5; // 固定进度条宽度(dp) + private OnProgressChangeListener listener; // 自定义监听器 + public CircularProgressBar(Context context) { super(context); init(); @@ -35,7 +37,7 @@ public class CircularProgressBar extends View { // 初始化用于绘制进度条的画笔 progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG); progressPaint.setStyle(Paint.Style.STROKE); - progressPaint.setStrokeWidth(dpToPx(progressWidth)); // 设置进度条宽度(10dp) + progressPaint.setStrokeWidth(dpToPx(progressWidth)); // 设置进度条宽度(dp) progressPaint.setColor(Color.WHITE); // 设置进度条颜色为白色 // 初始化用于绘制背景环的画笔 @@ -85,6 +87,9 @@ public class CircularProgressBar extends View { public void setProgress(float progress) { this.progress = Math.max(0, Math.min(progress, maxProgress)); // 限制进度值在 0 到 maxProgress 之间 + if (listener != null) { + listener.onProgressChanged(this, (int) progress); // 通知监听器进度变化 + } invalidate(); // 请求重新绘制视图 } @@ -94,6 +99,16 @@ public class CircularProgressBar extends View { invalidate(); // 请求重新绘制视图 } + // 添加设置监听器的方法 + public void setOnProgressChangeListener(OnProgressChangeListener listener) { + this.listener = listener; + } + + // 自定义进度变化监听器接口 + public interface OnProgressChangeListener { + void onProgressChanged(CircularProgressBar progressBar, int progress); + } + // 将 dp 转换为 px private int dpToPx(int dp) { float density = getResources().getDisplayMetrics().density; diff --git a/app/src/main/java/com/hi/music/player/javabean/A_data/AudioItem.java b/app/src/main/java/com/hi/music/player/javabean/A_data/AudioItem.java index 9a222ae..52068db 100644 --- a/app/src/main/java/com/hi/music/player/javabean/A_data/AudioItem.java +++ b/app/src/main/java/com/hi/music/player/javabean/A_data/AudioItem.java @@ -6,6 +6,7 @@ public class AudioItem implements Serializable { private String name; private String file; private String image; + private String duration; public AudioItem(String name, String file, String image) { this.name = name; @@ -36,4 +37,12 @@ public class AudioItem implements Serializable { public void setImage(String image) { this.image = image; } + + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } } diff --git a/app/src/main/java/com/hi/music/player/service/MusicPlayerForegroundService.java b/app/src/main/java/com/hi/music/player/service/MusicPlayerForegroundService.java new file mode 100644 index 0000000..2b68c98 --- /dev/null +++ b/app/src/main/java/com/hi/music/player/service/MusicPlayerForegroundService.java @@ -0,0 +1,166 @@ +package com.hi.music.player.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.ContentResolver; +import android.content.Intent; +import android.content.res.AssetFileDescriptor; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; +import androidx.lifecycle.MutableLiveData; + +import android.util.Log; + +import com.hi.music.player.R; +import com.hi.music.player.ui.activity.A_PlayActivity; + +import java.io.IOException; + +public class MusicPlayerForegroundService extends Service { + + public static final String CHANNEL_ID = "MusicPlayerChannel"; + private MediaPlayer mediaPlayer; + private final IBinder binder = new MusicBinder(); + private MutableLiveData isPlaying = new MutableLiveData<>(false); + + public class MusicBinder extends Binder { + public MusicPlayerForegroundService getService() { + return MusicPlayerForegroundService.this; + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String audioPath = intent.getStringExtra("Path"); + if (audioPath != null) { + initializePlayer(audioPath); + } + + startForeground(1, createNotification("Playing Audio", "Your audio is playing")); + + return START_NOT_STICKY; + } + + private Notification createNotification(String title, String content) { + createNotificationChannel(); + Intent notificationIntent = new Intent(this, A_PlayActivity.class); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(title) + .setContentText(content) + .setSmallIcon(R.drawable.home_select) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel = new NotificationChannel( + CHANNEL_ID, "Music Player Channel", NotificationManager.IMPORTANCE_LOW); + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(serviceChannel); + } + } + + private void initializePlayer(String path) { + try { + Log.d("MediaPlayerError","path: "+path); + + if (mediaPlayer == null) { + mediaPlayer = new MediaPlayer(); + } else { + mediaPlayer.reset(); + } + + if (path.startsWith("content://")) { + Uri contentUri = Uri.parse(path); + ContentResolver contentResolver = getContentResolver(); + AssetFileDescriptor afd = contentResolver.openAssetFileDescriptor(contentUri, "r"); + if (afd != null) { + mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + afd.close(); + } + } else { + AssetFileDescriptor afd = getAssets().openFd(path); + mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + afd.close(); + } + + mediaPlayer.prepare(); + mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start()); + isPlaying.postValue(true); + } catch (IOException e) { + Log.e("MediaPlayerError", "Could not open file: " + path, e); + } + } + + public void pauseAudio() { + if (mediaPlayer != null && mediaPlayer.isPlaying()) { + mediaPlayer.pause(); + isPlaying.postValue(false); + } + } + + public void resumeAudio() { + if (mediaPlayer != null && !mediaPlayer.isPlaying()) { + mediaPlayer.start(); + isPlaying.postValue(true); + } + } + + public MutableLiveData getIsPlaying() { + return isPlaying; + } + + // 添加获取当前播放位置的方法 + public int getCurrentPosition() { + if (mediaPlayer != null) { + return mediaPlayer.getCurrentPosition(); + } + return 0; // 如果 mediaPlayer 为 null,返回 0 + } + + // 添加获取总时长的方法 + public int getDuration() { + if (mediaPlayer != null) { + return mediaPlayer.getDuration(); + } + return 0; // 如果 mediaPlayer 为 null,返回 0 + } + + public void seekTo(int position) { + if (mediaPlayer != null) { + mediaPlayer.seekTo(position); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } + } +} diff --git a/app/src/main/java/com/hi/music/player/ui/activity/A_HomeActivity.java b/app/src/main/java/com/hi/music/player/ui/activity/A_HomeActivity.java index 4f030d2..7fe4375 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/A_HomeActivity.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/A_HomeActivity.java @@ -1,21 +1,24 @@ package com.hi.music.player.ui.activity; -import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import androidx.lifecycle.ViewModelProvider; + import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import com.hi.music.player.R; import com.hi.music.player.adapter.A_HomeViewPagerAdapter; import com.hi.music.player.databinding.ActivityAhomeBinding; import com.hi.music.player.databinding.HomeTabCustomBinding; +import com.hi.music.player.helper.CircularProgressBar; +import com.hi.music.player.ui.activity.viewmodel.A_VMPlay; import java.util.Objects; public class A_HomeActivity extends BaseActivity { - // 图标数组定义为类成员,避免重复 private final int[] defaultIcons = { R.drawable.home_unselect, R.drawable.import_unselect, @@ -26,11 +29,7 @@ public class A_HomeActivity extends BaseActivity { R.drawable.import_select, }; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - } + private A_VMPlay viewModel; @Override protected ActivityAhomeBinding getViewBinding() { @@ -39,9 +38,9 @@ public class A_HomeActivity extends BaseActivity { @Override protected void onCreateInit() { - + viewModel = new ViewModelProvider(this).get(A_VMPlay.class); initData(); - + setupObservers(); } @Override @@ -59,42 +58,73 @@ public class A_HomeActivity extends BaseActivity { return false; } - public void initData(){ - + public void initData() { A_HomeViewPagerAdapter adapter = new A_HomeViewPagerAdapter(this); vb.homeViewpager.setAdapter(adapter); + vb.homeViewpager.setUserInputEnabled(false); - // 设置TabLayout的图标 new TabLayoutMediator(vb.homeTabLayout, vb.homeViewpager, (tab, position) -> { HomeTabCustomBinding tabBinding = HomeTabCustomBinding.inflate(LayoutInflater.from(this)); tab.setCustomView(tabBinding.getRoot()); - tabBinding.homeIcon.setImageResource(defaultIcons[position]); // 默认图标 + tabBinding.homeIcon.setImageResource(defaultIcons[position]); }).attach(); - // 添加Tab选中与未选中事件监听器 vb.homeTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { - updateTabIcon(tab, true); // 更新选中的图标 + updateTabIcon(tab, true); } @Override public void onTabUnselected(TabLayout.Tab tab) { - updateTabIcon(tab, false); // 恢复未选中图标 + updateTabIcon(tab, false); } @Override public void onTabReselected(TabLayout.Tab tab) { - // 可选:重复选择Tab时的操作 } }); - // 设置默认选中第一个 TabLayout.Tab firstTab = vb.homeTabLayout.getTabAt(0); if (firstTab != null) { firstTab.select(); - updateTabIcon(firstTab, true); // 设置选中的图标 + updateTabIcon(firstTab, true); } + + vb.circularProgressBar.setMaxProgress(100); // 假设最大值为100 + + vb.circularProgressBar.setOnProgressChangeListener(new CircularProgressBar.OnProgressChangeListener() { + @Override + public void onProgressChanged(CircularProgressBar progressBar, int progress) { + viewModel.seekTo(progress); // 当用户改变进度时,调用 ViewModel 方法 + } + }); + + vb.pause.setOnClickListener(v -> { + viewModel.togglePlay(); + }); + } + + private void setupObservers() { + viewModel.getCurrentTime().observe(this, currentTime -> { + vb.bottomText.setText(currentTime); + }); + + viewModel.getTotalTime().observe(this, totalTime -> { + vb.topText.setText(totalTime); + }); + + viewModel.getProgress().observe(this, progress -> { + if (progress >= 0 && progress <= 100) { + vb.circularProgressBar.setProgress(progress); // 更新进度条 + } else { + Log.e("A_HomeActivity", "Progress out of range: " + progress); + } + }); + + viewModel.isPlaying().observe(this, isPlaying -> { + vb.pause.setImageResource(isPlaying ? R.drawable.play : R.drawable.pause); + }); } private void updateTabIcon(TabLayout.Tab tab, boolean isSelected) { @@ -108,4 +138,3 @@ public class A_HomeActivity extends BaseActivity { } } - diff --git a/app/src/main/java/com/hi/music/player/ui/activity/A_PlayActivity.java b/app/src/main/java/com/hi/music/player/ui/activity/A_PlayActivity.java index 77094ce..3c9357d 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/A_PlayActivity.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/A_PlayActivity.java @@ -1,23 +1,46 @@ package com.hi.music.player.ui.activity; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; -import android.media.MediaPlayer; +import android.content.ServiceConnection; +import android.os.IBinder; import android.util.Log; import android.view.View; -import android.widget.SeekBar; - -import androidx.lifecycle.ViewModelProvider; import com.hi.music.player.R; import com.hi.music.player.databinding.ActivityAplayBinding; import com.hi.music.player.javabean.A_data.AudioItem; -import com.hi.music.player.ui.activity.viewmodel.A_VMPlay; +import com.hi.music.player.service.MusicPlayerForegroundService; + +import java.util.Locale; public class A_PlayActivity extends BaseActivity { - private AudioItem audioItem; - private MediaPlayer mediaPlayer; - private A_VMPlay playViewModel; + private MusicPlayerForegroundService musicService; + private boolean isBound = false; + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + MusicPlayerForegroundService.MusicBinder binder = (MusicPlayerForegroundService.MusicBinder) service; + musicService = binder.getService(); + isBound = true; + + // 观察服务中的播放状态 + musicService.getIsPlaying().observe(A_PlayActivity.this, isPlaying -> { + vb.playButton.setBackgroundResource(isPlaying ? R.drawable.play : R.drawable.pause); + }); + + // 更新进度和总时长 + startUpdatingProgress(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + isBound = false; + } + }; @Override protected ActivityAplayBinding getViewBinding() { @@ -26,68 +49,74 @@ public class A_PlayActivity extends BaseActivity { @Override protected void onCreateInit() { - Intent intent = getIntent(); - audioItem = (AudioItem) intent.getSerializableExtra("Path"); + AudioItem audioItem = (AudioItem) intent.getSerializableExtra("Path"); if (audioItem == null) { + finish(); // 如果没有音频项目,结束活动 return; } - String path = audioItem.getFile(); - Log.d("------", "--------" + path); + // 启动前台服务 + Intent serviceIntent = new Intent(this, MusicPlayerForegroundService.class); + serviceIntent.putExtra("Path", audioItem.getFile()); + Log.d("A_PlayActivity", "Path: " + audioItem.getFile()); + startService(serviceIntent); + bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); - playViewModel = new ViewModelProvider(this).get(A_VMPlay.class); - mediaPlayer = new MediaPlayer(); - - playViewModel.initializePlayer(path); - - // 观察进度更新 - playViewModel.getProgress().observe(this, progress -> { - vb.songSeekbar.setProgress(progress); - }); - - // 观察播放状态 - playViewModel.isPlaying().observe(this, isPlaying -> { - vb.playButton.setBackgroundResource(isPlaying ? R.drawable.play : R.drawable.pause); - }); - - // 观察当前时间 - playViewModel.getCurrentTime().observe(this, currentTime -> { - vb.current.setText(currentTime); - }); - - // 观察总时间 - playViewModel.getTotalTime().observe(this, totalTime -> { - vb.time.setText(totalTime); + vb.playButton.setOnClickListener(v -> { + if (isBound) { + if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) { + musicService.pauseAudio(); + } else { + musicService.resumeAudio(); + } + } }); + // 启动定期更新播放进度 + startUpdatingProgress(); } @Override protected void onInitClick() { - vb.playButton.setOnClickListener(v -> playViewModel.togglePlay()); - - vb.songSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - playViewModel.seekTo(progress); // 拖动进度条时控制音频 - } - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - // 暂停更新进度 - } - - @Override - public void onStopTrackingTouch(SeekBar seekBar) { - // 恢复更新进度 - } - }); } + private void startUpdatingProgress() { + new Thread(() -> { + while (isBound && musicService != null) { + if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) { + int currentPosition = musicService.getCurrentPosition(); // 获取当前播放位置 + int duration = musicService.getDuration(); // 获取总时长 + runOnUiThread(() -> { + vb.songSeekbar.setProgress((int) ((currentPosition / (float) duration) * 100)); // 更新进度条 + vb.current.setText(formatTime(currentPosition)); // 更新当前时间文本 + vb.time.setText(formatTime(duration)); // 更新总时间文本 + }); + } + try { + Thread.sleep(1000); // 每秒更新一次 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }).start(); + } + + private String formatTime(int milliseconds) { + int minutes = (milliseconds / 1000) / 60; + int seconds = (milliseconds / 1000) % 60; + return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (isBound) { + unbindService(serviceConnection); + isBound = false; + } + } @Override public boolean isFullScreen() { diff --git a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/A_VMPlay.java b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/A_VMPlay.java index bd64ae4..6111335 100644 --- a/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/A_VMPlay.java +++ b/app/src/main/java/com/hi/music/player/ui/activity/viewmodel/A_VMPlay.java @@ -1,26 +1,33 @@ package com.hi.music.player.ui.activity.viewmodel; import android.app.Application; -import android.content.res.AssetFileDescriptor; -import android.media.MediaPlayer; -import android.util.Log; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; -import java.io.IOException; +import com.hi.music.player.service.MusicPlayerForegroundService; + +import java.util.Locale; public class A_VMPlay extends AndroidViewModel { - private MediaPlayer mediaPlayer; private final MutableLiveData currentTime = new MutableLiveData<>(); private final MutableLiveData totalTime = new MutableLiveData<>(); + private final MutableLiveData fileName = new MutableLiveData<>(); private final MutableLiveData isPlaying = new MutableLiveData<>(false); private final MutableLiveData progress = new MutableLiveData<>(0); // 添加进度 LiveData + private MusicPlayerForegroundService musicService; + private boolean isBound = false; + public A_VMPlay(Application application) { super(application); - mediaPlayer = new MediaPlayer(); + bindToMusicService(); } // 获取进度的 LiveData @@ -40,37 +47,54 @@ public class A_VMPlay extends AndroidViewModel { return isPlaying; } - public void initializePlayer(String path) { - try { - AssetFileDescriptor afd = getApplication().getAssets().openFd(path); - mediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); - mediaPlayer.prepare(); - mediaPlayer.setOnPreparedListener(mp -> { - totalTime.setValue(formatTime(mediaPlayer.getDuration())); - startUpdatingProgress(); - }); - } catch (IOException e) { - Log.e("MediaPlayerError", "Could not open asset file: " + path, e); - } + public LiveData getFileName() { + return fileName; } + private void bindToMusicService() { + Intent serviceIntent = new Intent(getApplication(), MusicPlayerForegroundService.class); + getApplication().bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + MusicPlayerForegroundService.MusicBinder binder = (MusicPlayerForegroundService.MusicBinder) service; + musicService = binder.getService(); + isBound = true; + + // 观察服务中的播放状态 + musicService.getIsPlaying().observeForever(isPlaying::setValue); + + // 开始监听播放进度 + startUpdatingProgress(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + isBound = false; + } + }; + public void togglePlay() { - if (mediaPlayer.isPlaying()) { - mediaPlayer.pause(); - isPlaying.setValue(false); - } else { - mediaPlayer.start(); - isPlaying.setValue(true); + if (isBound) { + if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) { + musicService.pauseAudio(); + } else { + musicService.resumeAudio(); + } } } private void startUpdatingProgress() { new Thread(() -> { - while (mediaPlayer != null) { - if (mediaPlayer.isPlaying()) { - int currentPosition = mediaPlayer.getCurrentPosition(); + while (isBound && musicService != null) { + if (musicService.getIsPlaying().getValue() != null && musicService.getIsPlaying().getValue()) { + int currentPosition = musicService.getCurrentPosition(); // 从服务获取当前播放位置 currentTime.postValue(formatTime(currentPosition)); - progress.postValue((int) ((currentPosition / (float) mediaPlayer.getDuration()) * 100)); // 更新进度 + int duration = musicService.getDuration(); // 从服务获取音频总时长 + progress.postValue((int) ((currentPosition / (float) duration) * 100)); // 更新进度 + totalTime.postValue(formatTime(duration)); } try { Thread.sleep(1000); @@ -82,24 +106,25 @@ public class A_VMPlay extends AndroidViewModel { } public void seekTo(int progress) { - if (mediaPlayer != null) { - int duration = mediaPlayer.getDuration(); - mediaPlayer.seekTo((int) (duration * progress / 100.0)); + if (isBound && musicService != null) { + int duration = musicService.getDuration(); + musicService.seekTo((int) (duration * progress / 100.0)); } } private String formatTime(int milliseconds) { int minutes = (milliseconds / 1000) / 60; int seconds = (milliseconds / 1000) % 60; - return String.format("%02d:%02d", minutes, seconds); + return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds); } @Override protected void onCleared() { super.onCleared(); - if (mediaPlayer != null) { - mediaPlayer.release(); - mediaPlayer = null; + if (isBound) { + getApplication().unbindService(serviceConnection); + isBound = false; } } } + diff --git a/app/src/main/java/com/hi/music/player/ui/fragmnt/A_ImportFragment.java b/app/src/main/java/com/hi/music/player/ui/fragmnt/A_ImportFragment.java index 4e39bb1..f42667c 100644 --- a/app/src/main/java/com/hi/music/player/ui/fragmnt/A_ImportFragment.java +++ b/app/src/main/java/com/hi/music/player/ui/fragmnt/A_ImportFragment.java @@ -7,19 +7,30 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; +import android.os.Environment; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.hi.music.player.adapter.A_ImportFragmentAdapter; import com.hi.music.player.databinding.FragmentAImportBinding; +import com.hi.music.player.javabean.A_data.AudioItem; import com.hi.music.player.ui.activity.A_SettingActivity; +import com.hi.music.player.ui.fragmnt.viewmodel.A_VMImport; + +import java.io.IOException; +import java.util.List; public class A_ImportFragment extends BaseFragment { private static final int REQUEST_CODE_READ_MEDIA_AUDIO = 1001; private static final int REQUEST_CODE_PICK_AUDIO = 1002; + private A_ImportFragmentAdapter adapter; + private A_VMImport viewModel; @Override protected FragmentAImportBinding getFragmentVb() { @@ -28,47 +39,59 @@ public class A_ImportFragment extends BaseFragment { @Override protected void initView() { + viewModel = new ViewModelProvider(this).get(A_VMImport.class); + adapter = new A_ImportFragmentAdapter(requireContext()); - initData(); + Vb.importRecycler.setLayoutManager(new LinearLayoutManager(getContext())); + Vb.importRecycler.setAdapter(adapter); + + // 监听音频文件列表变化 + viewModel.getAudioFiles().observe(getViewLifecycleOwner(), this::updateAudioList); initEvent(); - } - public void initData() { - + // 更新音频文件列表 + private void updateAudioList(List audioFiles) { + adapter.setAudioFiles(audioFiles); } + // 初始化事件 public void initEvent() { - Vb.setting.setOnClickListener(v -> { Intent intent = new Intent(getContext(), A_SettingActivity.class); startActivity(intent); }); - Vb.add.setOnClickListener(v -> { - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_MEDIA_AUDIO) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(requireActivity(), - new String[]{Manifest.permission.READ_MEDIA_AUDIO}, - REQUEST_CODE_READ_MEDIA_AUDIO); - } else { - openAudioPicker(); - } - } else { - if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(requireActivity(), - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_CODE_READ_MEDIA_AUDIO); - } else { - openAudioPicker(); - } - } - + // 添加按钮的点击事件,打开文件选择器 + Vb.add.setOnClickListener(v -> checkAndOpenAudioPicker()); + // 适配器中的删除按钮点击事件 + adapter.setOnDeleteClickListener(filePath -> { + viewModel.markAudioAsDeleted(filePath); + Toast.makeText(getContext(), "Audio marked as deleted", Toast.LENGTH_SHORT).show(); }); + } + // 检查并请求权限,然后打开音频选择器 + private void checkAndOpenAudioPicker() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_MEDIA_AUDIO) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(requireActivity(), + new String[]{Manifest.permission.READ_MEDIA_AUDIO}, REQUEST_CODE_READ_MEDIA_AUDIO); + } else { + openAudioPicker(); // 权限已授予,打开选择器 + } + } else { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(requireActivity(), + new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_CODE_READ_MEDIA_AUDIO); + } else { + openAudioPicker(); // 权限已授予,打开选择器 + } + } } // 处理权限请求结果 @@ -77,6 +100,7 @@ public class A_ImportFragment extends BaseFragment { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_READ_MEDIA_AUDIO) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // 权限已授予,打开音频选择器 openAudioPicker(); } else { Toast.makeText(requireContext(), "Permission denied", Toast.LENGTH_SHORT).show(); @@ -86,26 +110,48 @@ public class A_ImportFragment extends BaseFragment { // 打开音频选择器 private void openAudioPicker() { + // 检查外部存储状态 + String state = Environment.getExternalStorageState(); + if (!Environment.MEDIA_MOUNTED.equals(state)) { + Toast.makeText(getContext(), "外部存储不可用", Toast.LENGTH_LONG).show(); + return; + } + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); // 持久 URI 权限 intent.setType("audio/*"); // 设置 MIME 类型为音频 intent.addCategory(Intent.CATEGORY_OPENABLE); // 限制为可打开的文件 + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 授予 URI 读取权限 startActivityForResult(intent, REQUEST_CODE_PICK_AUDIO); } - - // 处理 Activity 结果 + // 处理音频选择器结果 @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_PICK_AUDIO && resultCode == RESULT_OK) { if (data != null) { - Uri audioUri = data.getData(); - // 处理音频文件,例如播放或显示在列表中 - Toast.makeText(requireContext(), "Audio Selected: " + audioUri.toString(), Toast.LENGTH_SHORT).show(); + Uri selectedAudioUri = data.getData(); + if (selectedAudioUri != null) { + // 授予 URI 读取权限 + requireActivity().grantUriPermission(requireActivity().getPackageName(), selectedAudioUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + + // 将音频添加到 ViewModel 中 + try { + viewModel.addAudioFile(selectedAudioUri); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } } - + // 刷新音频列表 + @Override + public void onResume() { + super.onResume(); + // 重新获取音频文件列表并更新 UI + viewModel.getAudioFiles().observe(getViewLifecycleOwner(), this::updateAudioList); + } } - diff --git a/app/src/main/java/com/hi/music/player/ui/fragmnt/viewmodel/A_VMImport.java b/app/src/main/java/com/hi/music/player/ui/fragmnt/viewmodel/A_VMImport.java index cd90a5d..01fb1cb 100644 --- a/app/src/main/java/com/hi/music/player/ui/fragmnt/viewmodel/A_VMImport.java +++ b/app/src/main/java/com/hi/music/player/ui/fragmnt/viewmodel/A_VMImport.java @@ -1,9 +1,127 @@ package com.hi.music.player.ui.fragmnt.viewmodel; -import androidx.lifecycle.ViewModel; +import android.app.Application; +import android.database.Cursor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.provider.MediaStore; -public class A_VMImport extends ViewModel { +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.MutableLiveData; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.hi.music.player.javabean.A_data.AudioItem; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import android.content.SharedPreferences; +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +public class A_VMImport extends AndroidViewModel { + + private static final String PREFS_NAME = "audio_files_prefs"; + private static final String AUDIO_FILES_KEY = "audio_files"; + + private final MutableLiveData> audioFilesLiveData = new MutableLiveData<>(); + private final SharedPreferences sharedPreferences; + private final Gson gson; + private final List audioFiles; + + public A_VMImport(Application application) { + super(application); + sharedPreferences = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + gson = new Gson(); + audioFiles = loadAudioFiles(); // 加载已保存的音频文件列表 + audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 设置 LiveData 的初始值 + } + + // 添加选中的音频文件 + public void addAudioFile(Uri audioUri) throws IOException { + String fileName = null; + long duration = 0; + + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setDataSource(getApplication(), audioUri); + fileName = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (durationStr != null) { + duration = Long.parseLong(durationStr); + } + } catch (Exception e) { + Log.e("ViewModel", "Failed to retrieve audio metadata", e); + } finally { + retriever.release(); // 释放资源 + } + + // 如果 fileName 为空,使用 URI 的最后一部分作为文件名 + if (fileName == null || fileName.isEmpty()) { + fileName = audioUri.getLastPathSegment(); + } + + if (duration > 0) { + AudioItem newItem = new AudioItem(fileName, audioUri.toString(), ""); + newItem.setDuration(formatDuration(duration)); // 格式化时长 + + // 防止重复添加 + if (!audioFiles.contains(newItem)) { + audioFiles.add(newItem); + saveAudioFiles(); // 每次添加后保存到 SharedPreferences + audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 更新 LiveData + } else { + Toast.makeText(getApplication(), "Audio file already exists in the list.", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getApplication(), "Failed to retrieve audio duration.", Toast.LENGTH_SHORT).show(); + } + } + // 保存音频文件列表到 SharedPreferences + private void saveAudioFiles() { + SharedPreferences.Editor editor = sharedPreferences.edit(); + String json = gson.toJson(audioFiles); + editor.putString(AUDIO_FILES_KEY, json); + editor.apply(); + } + + // 从 SharedPreferences 加载音频文件列表 + private List loadAudioFiles() { + String json = sharedPreferences.getString(AUDIO_FILES_KEY, null); + Type type = new TypeToken>() {}.getType(); + List loadedFiles = gson.fromJson(json, type); + return loadedFiles != null ? loadedFiles : new ArrayList<>(); // 返回加载的文件列表或空列表 + } + + // 标记音频文件为已删除 + public void markAudioAsDeleted(String filePath) { + // 从列表中移除该音频 + for (AudioItem item : new ArrayList<>(audioFiles)) { + if (item.getFile().equals(filePath)) { + audioFiles.remove(item); + break; + } + } + + // 更新 LiveData + audioFilesLiveData.setValue(new ArrayList<>(audioFiles)); // 更新 LiveData + saveAudioFiles(); // 更新 SharedPreferences + } + + // 获取音频文件列表 + public MutableLiveData> getAudioFiles() { + return audioFilesLiveData; + } + + // 格式化时长 + private String formatDuration(long duration) { + long minutes = (duration / 1000) / 60; + long seconds = (duration / 1000) % 60; + return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds); + } } diff --git a/app/src/main/res/drawable/options.xml b/app/src/main/res/drawable/options.xml new file mode 100644 index 0000000..80184ea --- /dev/null +++ b/app/src/main/res/drawable/options.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_ahome.xml b/app/src/main/res/layout/activity_ahome.xml index b097461..0bfe410 100644 --- a/app/src/main/res/layout/activity_ahome.xml +++ b/app/src/main/res/layout/activity_ahome.xml @@ -33,7 +33,6 @@ android:layout_marginBottom="-5dp" android:background="@drawable/round_rectangle" android:backgroundTint="#80F988" - android:visibility="gone" app:layout_constraintBottom_toTopOf="@+id/home_tab_layout"> @@ -60,7 +59,7 @@ android:layout_width="48dp" android:layout_height="48dp" android:layout_gravity="center" - android:src="@mipmap/cover" /> + android:src="@mipmap/default_image" /> diff --git a/app/src/main/res/layout/activity_aplay.xml b/app/src/main/res/layout/activity_aplay.xml index 8099962..a6b0e2d 100644 --- a/app/src/main/res/layout/activity_aplay.xml +++ b/app/src/main/res/layout/activity_aplay.xml @@ -36,7 +36,7 @@ android:layout_height="300dp" android:layout_marginTop="66dp" android:contentDescription="Record" - android:src="@mipmap/cover" + android:src="@mipmap/default_image" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/now_playing_text" /> diff --git a/app/src/main/res/layout/fragment_a_import.xml b/app/src/main/res/layout/fragment_a_import.xml index 6222ef5..9495e53 100644 --- a/app/src/main/res/layout/fragment_a_import.xml +++ b/app/src/main/res/layout/fragment_a_import.xml @@ -14,6 +14,7 @@ android:layout_marginTop="32sp" android:text="Parents voice" android:textSize="32sp" + android:textColor="@color/white" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -30,7 +31,7 @@ app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/item_a_home_1.xml b/app/src/main/res/layout/item_a_home_1.xml index 83f2070..34c14b9 100644 --- a/app/src/main/res/layout/item_a_home_1.xml +++ b/app/src/main/res/layout/item_a_home_1.xml @@ -16,7 +16,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" - android:src="@mipmap/cover" /> + android:src="@mipmap/default_image" /> + android:src="@mipmap/default_image" /> diff --git a/app/src/main/res/layout/item_a_home_3.xml b/app/src/main/res/layout/item_a_home_3.xml index 395777d..2dce966 100644 --- a/app/src/main/res/layout/item_a_home_3.xml +++ b/app/src/main/res/layout/item_a_home_3.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" - android:src="@mipmap/cover" /> + android:src="@mipmap/default_image" /> diff --git a/app/src/main/res/layout/item_a_import.xml b/app/src/main/res/layout/item_a_import.xml new file mode 100644 index 0000000..7e363ec --- /dev/null +++ b/app/src/main/res/layout/item_a_import.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-xxxhdpi/default_image.png b/app/src/main/res/mipmap-xxxhdpi/default_image.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e6713a5e238d8c02fba0a35cb3be4d9f2ab18e GIT binary patch literal 27859 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

I%{bP%hoxCv`VvbY9Z&- zGf}oNpR>1qrck1s{CifF_9D&l<%Kqkxz2&B3eM!Pq<*@RDvAi{mS76n0J!wNH<>_`GI8Uq?XxB{lUmp+)mLqb1SsFMpx`-lI(UpQZsZ54#&R?Du(<}{RyYLU$v&#~5E zX(+;`*JY_it6wXseC8J(|8qNv#GSfv=49FF0v{JZt*%($dMTUb``XVgJonOW*Iwhr z2E$y3#OL53TFSD{Ksx zkGATSTND_!FT-qM^%oDHp3a|p_4;#TI#|c(=CPhPx(1~)*;rWEKBWxxygal0vDFfO z`gyao2A6C9ku_!V$^y=w9+daaGC=5uO#a}nzdX))>m0Of;)Xe?#hF-3n=&?SC5G&o zNZ~WIs9~EYIJH=AZY;6v!4P<}*u=HJ_U=FX((TT_{TQ7ufO~Aw-8w&VJ9pkp+xe3} z_l{59wyf(j^zlLtZmn}3xvff%v-h zdnQg52KBdVCFZutsIf#z0?BoNLnX6F^sp%U^D~-FE*?I8TgCal-~8$I67*fkjcp&b zPNFJc%Cmo1Ro9{$Km@ z*-ZIJvzg6m*l z3_kEfA}jyT-&!rYdL9r$a!8C#bGT|g=F6WUWmA(=isAxoow@kieqiHH=RR@$?9#yz zy;9CKmeAH_hTRFS-Gt-C+gCNM6C;^dXDG~c>cz#+ZonXWmcar)1hR7Vrz8T z#^0toR;j6DRb~lSS?o8I)kH{c#*3uYS^mrh@Rdyv%|pIj|{$r_i@;d~P+ z9Z9Ce)t@^wnZh7%S_U8Z0ha&t>ZS3BwtuzpRH*sZp#E(fmgqik1J$!D#o~VX*7i5g zZ5%!Iw!qWX8_$gkQB@f8;y1I;JFUYAFC3a|z#wm#3>NqSm6fZPmTqOb)Vx||oGTob ze1DKKtL#lm^228N@e`ozF6-v=2K_yLvI zb6STs4{mPZAc_3Sgk$nY`C3_Zv2a;mzI60^HsAq~KT+ewM9Varof$p8b#6Y7dXTqH z1_gcq<@f#H|NF_D^BPK$REwLoK?8;SLD(oJcVko_^2X6WJcS2P3PhNGW9v7M-Yru- z$i2(p13zH0eDy2i16##+OnE6$9f3qwilu!O7GdM7M~_cokU@eB7We^^gE#BH>G)T_ v{kx|y$RL9ZGRPo<3^K?dgA6jrTPgnz9Q@&qLnhy-00000NkvXXu0mjf|7=_V literal 0 HcmV?d00001