feat: SongPickerActivity
This commit is contained in:
@@ -85,6 +85,8 @@ dependencies {
|
|||||||
implementation libs.androidx.ui.graphics
|
implementation libs.androidx.ui.graphics
|
||||||
implementation libs.androidx.ui.tooling.preview
|
implementation libs.androidx.ui.tooling.preview
|
||||||
implementation libs.androidx.material3
|
implementation libs.androidx.material3
|
||||||
|
implementation libs.androidx.recyclerview
|
||||||
|
implementation libs.androidx.appcompat
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
androidTestImplementation libs.androidx.junit
|
androidTestImplementation libs.androidx.junit
|
||||||
androidTestImplementation libs.androidx.espresso.core
|
androidTestImplementation libs.androidx.espresso.core
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="at.lockstep.ui.SongPickerActivity" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="at.lockstep.app.LstForegroundService"
|
android:name="at.lockstep.app.LstForegroundService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
|||||||
@@ -5,15 +5,37 @@ import android.content.Intent;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Button;
|
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 androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
import at.lockstep.R;
|
import at.lockstep.R;
|
||||||
import at.lockstep.pb.PlaybackEngine;
|
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 btnStart;
|
||||||
private Button btnStop;
|
private Button btnStop;
|
||||||
private Button btnMediaStoreBenchmark;
|
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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -23,6 +45,7 @@ public class MainActivity extends Activity {
|
|||||||
btnStart = findViewById(R.id.btnStart);
|
btnStart = findViewById(R.id.btnStart);
|
||||||
btnStop = findViewById(R.id.btnStop);
|
btnStop = findViewById(R.id.btnStop);
|
||||||
btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark);
|
btnMediaStoreBenchmark = findViewById(R.id.btnMediaStoreBenchmark);
|
||||||
|
btnPickSong = findViewById(R.id.btnPickSong);
|
||||||
|
|
||||||
// TODO: handle clicking START button twice
|
// TODO: handle clicking START button twice
|
||||||
btnStart.setOnClickListener(v ->
|
btnStart.setOnClickListener(v ->
|
||||||
@@ -47,5 +70,10 @@ public class MainActivity extends Activity {
|
|||||||
Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class);
|
Intent intent = new Intent(MainActivity.this, MediaStoreBenchmarkActivity.class);
|
||||||
startActivity(intent);
|
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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Benchmark media store" />
|
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>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<style name="Theme.Lockstep" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
||||||
<style name="Theme.Lockstep" parent="android:Theme.Material.Light.NoActionBar" />
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -11,6 +11,8 @@ composeBom = "2024.04.01"
|
|||||||
logbackAndroid = "2.0.0"
|
logbackAndroid = "2.0.0"
|
||||||
oboe = "1.10.0"
|
oboe = "1.10.0"
|
||||||
slf4jApi = "1.7.30"
|
slf4jApi = "1.7.30"
|
||||||
|
recyclerview = "1.3.1"
|
||||||
|
appcompat = "1.7.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -30,6 +32,8 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
|
|||||||
logback-android = { module = "com.github.tony19:logback-android", version.ref = "logbackAndroid" }
|
logback-android = { module = "com.github.tony19:logback-android", version.ref = "logbackAndroid" }
|
||||||
oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
|
oboe = { module = "com.google.oboe:oboe", version.ref = "oboe" }
|
||||||
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
|
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4jApi" }
|
||||||
|
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
||||||
|
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user