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 0000000..a4e6713 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/default_image.png differ