feat: SongPickerActivity
This commit is contained in:
@@ -85,6 +85,8 @@ dependencies {
|
||||
implementation libs.androidx.ui.graphics
|
||||
implementation libs.androidx.ui.tooling.preview
|
||||
implementation libs.androidx.material3
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.androidx.appcompat
|
||||
testImplementation libs.junit
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="at.lockstep.ui.SongPickerActivity" />
|
||||
|
||||
<service
|
||||
android:name="at.lockstep.app.LstForegroundService"
|
||||
android:exported="false"
|
||||
|
||||
@@ -5,15 +5,37 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import at.lockstep.R;
|
||||
import at.lockstep.pb.PlaybackEngine;
|
||||
import at.lockstep.ui.SongPickerActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private Button btnStart;
|
||||
private Button btnStop;
|
||||
private Button btnMediaStoreBenchmark;
|
||||
private Button btnPickSong;
|
||||
private final ActivityResultLauncher<Intent> launcher;
|
||||
|
||||
public MainActivity() {
|
||||
launcher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
String contentUri = data.getStringExtra("content_uri");
|
||||
Toast.makeText(this, "Item clicked: " + contentUri, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -23,6 +45,7 @@ public class MainActivity extends Activity {
|
||||
btnStart = findViewById(R.id.btnStart);
|
||||
btnStop = findViewById(R.id.btnStop);
|
||||
btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark);
|
||||
btnPickSong = findViewById(R.id.btnPickSong);
|
||||
|
||||
// TODO: handle clicking START button twice
|
||||
btnStart.setOnClickListener(v ->
|
||||
@@ -47,5 +70,10 @@ public class MainActivity extends Activity {
|
||||
Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class);
|
||||
startActivity(intent);
|
||||
});
|
||||
|
||||
btnPickSong.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, SongPickerActivity.class);
|
||||
launcher.launch(intent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
40
app/src/main/java/at/lockstep/ui/SongItem.java
Normal file
40
app/src/main/java/at/lockstep/ui/SongItem.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package at.lockstep.ui;
|
||||
|
||||
/**
|
||||
* Item DTO for song picker.
|
||||
*/
|
||||
public class SongItem {
|
||||
private String title;
|
||||
private String artist;
|
||||
private String contentUri;
|
||||
|
||||
public SongItem(String title, String artist, String contentUri) {
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public void setArtist(String artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
|
||||
public String getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
public void setContentUri(String contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
}
|
||||
170
app/src/main/java/at/lockstep/ui/SongPickerActivity.java
Normal file
170
app/src/main/java/at/lockstep/ui/SongPickerActivity.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package at.lockstep.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import at.lockstep.R;
|
||||
|
||||
/**
|
||||
* Choose a song from the device library.
|
||||
*/
|
||||
public class SongPickerActivity extends Activity implements SongPickerAdapter.OnItemClickListener {
|
||||
private List<SongItem> songs = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_song_picker);
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recyclerView);
|
||||
|
||||
if (hasReadPermission()) {
|
||||
loadSongList();
|
||||
} else {
|
||||
requestReadPermission();
|
||||
}
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(new SongPickerAdapter(songs, this));
|
||||
}
|
||||
|
||||
private static final int REQUEST_READ_PERMISSION = 1001;
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode,
|
||||
String[] permissions,
|
||||
int[] grantResults
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == REQUEST_READ_PERMISSION) {
|
||||
boolean granted = grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if (granted) {
|
||||
loadSongList();
|
||||
} else {
|
||||
songs.add(new SongItem("Have No Songs - Permission denied", "Re-open app and try again.", null));
|
||||
//resultTextView.setText("Permission denied.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasReadPermission() {
|
||||
String permission;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permission = Manifest.permission.READ_MEDIA_AUDIO;
|
||||
} else {
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
}
|
||||
|
||||
return ContextCompat.checkSelfPermission(this, permission)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private void requestReadPermission() {
|
||||
String permission;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permission = Manifest.permission.READ_MEDIA_AUDIO;
|
||||
} else {
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
}
|
||||
|
||||
requestPermissions(
|
||||
new String[]{ permission },
|
||||
REQUEST_READ_PERMISSION
|
||||
);
|
||||
}
|
||||
|
||||
private void loadSongList() {
|
||||
// TODO: cleanup, remove this list, etc
|
||||
List<String> musicList = new ArrayList<>();
|
||||
|
||||
android.net.Uri collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
|
||||
String[] projection = new String[] {
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.TITLE,
|
||||
MediaStore.Audio.Media.ARTIST,
|
||||
MediaStore.Audio.Media.DATA
|
||||
};
|
||||
|
||||
String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
|
||||
|
||||
long start = SystemClock.elapsedRealtime();
|
||||
|
||||
//
|
||||
// perform MediaStore query
|
||||
//
|
||||
try (android.database.Cursor cursor = getContentResolver().query(
|
||||
collection,
|
||||
projection,
|
||||
selection,
|
||||
null,
|
||||
MediaStore.Audio.Media.TITLE + " ASC"
|
||||
)) {
|
||||
if (cursor != null) {
|
||||
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID);
|
||||
int titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE);
|
||||
int artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST);
|
||||
int dataColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
String contentUri = cursor.getString(idColumn); // the content:// Uri for the MediaStore item
|
||||
String title = cursor.getString(titleColumn);
|
||||
String artist = cursor.getString(artistColumn);
|
||||
String path = dataColumn != -1 ? cursor.getString(dataColumn) : null;
|
||||
|
||||
if (path != null) {
|
||||
musicList.add(title + "\n" + path);
|
||||
songs.add(new SongItem(title, artist, contentUri));
|
||||
} else {
|
||||
musicList.add(title + "\n[path unavailable]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long elapsedMs = SystemClock.elapsedRealtime() - start;
|
||||
|
||||
StringBuilder header = new StringBuilder();
|
||||
header.append("Found ").append(musicList.size()).append(" music files\n");
|
||||
header.append("Query time: ").append(elapsedMs).append(" ms\n\n");
|
||||
|
||||
//resultTextView.setText(header.toString() + String.join("\n\n", musicList));
|
||||
|
||||
// 200 music files in 32 ms
|
||||
// paths like "/storage/emulated/0/Download/...mp3"
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(SongItem item) {
|
||||
if(item.getContentUri() == null) {
|
||||
// clicked the prompt for missing permissions?
|
||||
// try acquiring them again.
|
||||
// nice-to: test this.
|
||||
requestReadPermission();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent data = new Intent();
|
||||
data.putExtra("content_uri", item.getContentUri());
|
||||
setResult(Activity.RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
66
app/src/main/java/at/lockstep/ui/SongPickerAdapter.java
Normal file
66
app/src/main/java/at/lockstep/ui/SongPickerAdapter.java
Normal file
@@ -0,0 +1,66 @@
|
||||
package at.lockstep.ui;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import at.lockstep.R;
|
||||
|
||||
/**
|
||||
* RecyclerView Adapter for song picker.
|
||||
*/
|
||||
public class SongPickerAdapter extends RecyclerView.Adapter<SongPickerAdapter.SongPickerViewHolder> {
|
||||
private List<SongItem> songList;
|
||||
private final OnItemClickListener listener;
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(SongItem item);
|
||||
}
|
||||
public SongPickerAdapter(List<SongItem> songList, OnItemClickListener listener) {
|
||||
this.songList = songList;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public SongPickerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.song_card, parent, false);
|
||||
return new SongPickerViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SongPickerViewHolder holder, int position) {
|
||||
SongItem songItem = songList.get(position);
|
||||
holder.songTitle.setText(songItem.getTitle());
|
||||
holder.songArtist.setText(songItem.getArtist());
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
listener.onItemClick(songItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return songList.size();
|
||||
}
|
||||
|
||||
static class SongPickerViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView songTitle;
|
||||
TextView songArtist;
|
||||
public SongPickerViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
songTitle = itemView.findViewById(R.id.songTitle);
|
||||
songArtist = itemView.findViewById(R.id.songArtist);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,12 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Benchmark media store" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnPickSong"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="Pick song" />
|
||||
|
||||
</LinearLayout>
|
||||
28
app/src/main/res/layout/activity_song_picker.xml
Normal file
28
app/src/main/res/layout/activity_song_picker.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="@android:color/background_light"
|
||||
tools:context=".ui.SongPickerActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Song Picker"
|
||||
android:textAlignment="center"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:paddingTop="54sp"
|
||||
android:paddingBottom="20sp"
|
||||
android:textSize="24sp"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:overScrollMode="always"/>
|
||||
|
||||
</LinearLayout>
|
||||
34
app/src/main/res/layout/song_card.xml
Normal file
34
app/src/main/res/layout/song_card.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
>
|
||||
<!-- android:layout_height="96dp" -->
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/songTitle"
|
||||
android:textSize="16sp"
|
||||
android:text="Song Title"
|
||||
android:textColor="@color/black"
|
||||
android:maxLines="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/songArtist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/songTitle"
|
||||
android:text="Artist name"
|
||||
android:textSize="16sp"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.Lockstep" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
<style name="Theme.Lockstep" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||
</resources>
|
||||
Reference in New Issue
Block a user