mirror of
https://github.com/Michatec/Radio.git
synced 2026-01-31 07:20:40 +00:00
Compare commits
7 Commits
14
...
9b5c7e3c04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b5c7e3c04 | ||
|
|
4466654e92 | ||
|
|
b2de7bd534 | ||
|
|
d3dfcb98f9 | ||
|
|
0adb906438 | ||
|
|
49e63d3aaa | ||
|
|
f892a137ce |
11
.github/workflows/gradle-publish.yml
vendored
11
.github/workflows/gradle-publish.yml
vendored
@@ -51,6 +51,10 @@ jobs:
|
||||
- name: Check APK path
|
||||
run: ls -R app/build/outputs/apk
|
||||
|
||||
- name: Zipalign APK
|
||||
run: |
|
||||
/usr/local/lib/android/sdk/build-tools/34.0.0/zipalign -v -p 4 ${{ env.APK_PATH }} app-release-aligned.apk
|
||||
|
||||
- name: Sign APK
|
||||
env:
|
||||
SIGN_CERT: ${{ secrets.SIGN_CERT }}
|
||||
@@ -58,13 +62,8 @@ jobs:
|
||||
run: |
|
||||
echo "$SIGN_CERT" | base64 -d > cert.der
|
||||
echo "$SIGN_KEY" | base64 -d > key.der
|
||||
mv ${{ env.APK_PATH }} app-release.apk
|
||||
${{ env.APKSIGNER }} sign --key key.der --cert cert.der app-release.apk
|
||||
${{ env.APKSIGNER }} sign --key key.der --cert cert.der app-release-aligned.apk
|
||||
rm cert.der key.der
|
||||
|
||||
- name: Zipalign APK
|
||||
run: |
|
||||
/usr/local/lib/android/sdk/build-tools/34.0.0/zipalign -v -p 4 app-release.apk app-release-aligned.apk
|
||||
mv app-release-aligned.apk app-release.apk
|
||||
|
||||
- name: Upload artifact
|
||||
|
||||
@@ -19,8 +19,8 @@ android {
|
||||
applicationId 'com.michatec.radio'
|
||||
minSdk 28
|
||||
targetSdk 36
|
||||
versionCode 140
|
||||
versionName '14'
|
||||
versionCode 141
|
||||
versionName '14.1'
|
||||
resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr']
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.navigation.ui.NavigationUI
|
||||
import androidx.navigation.ui.navigateUp
|
||||
import com.michatec.radio.helpers.AppThemeHelper
|
||||
import com.michatec.radio.helpers.FileHelper
|
||||
import com.michatec.radio.helpers.ImportHelper
|
||||
import com.michatec.radio.helpers.PreferencesHelper
|
||||
|
||||
|
||||
@@ -41,18 +40,6 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// house-keeping: determine if edit stations is enabled by default todo: remove in 2023
|
||||
if (PreferencesHelper.isHouseKeepingNecessary()) {
|
||||
// house-keeping 1: remove hard coded default image
|
||||
ImportHelper.removeDefaultStationImageUris(this)
|
||||
// house-keeping 2: if existing user detected, enable Edit Stations by default
|
||||
if (PreferencesHelper.loadCollectionSize() != -1) {
|
||||
// existing user detected - enable Edit Stations by default
|
||||
PreferencesHelper.saveEditStationsEnabled(true)
|
||||
}
|
||||
PreferencesHelper.saveHouseKeepingNecessaryState()
|
||||
}
|
||||
|
||||
// set up views
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
@@ -193,6 +194,8 @@ class CollectionAdapter(
|
||||
setStationImage(stationViewHolder, station)
|
||||
setStationButtons(stationViewHolder, station)
|
||||
setEditViews(stationViewHolder, station)
|
||||
setPlaybackProgress(stationViewHolder, station)
|
||||
setDownloadProgress(stationViewHolder, station)
|
||||
|
||||
// show / hide edit views
|
||||
when (expandedStationPosition) {
|
||||
@@ -248,6 +251,28 @@ class CollectionAdapter(
|
||||
}
|
||||
|
||||
|
||||
/* Sets the playback progress view */
|
||||
private fun setPlaybackProgress(stationViewHolder: StationViewHolder, station: Station) {
|
||||
if (station.bufferingProgress > 0) {
|
||||
stationViewHolder.bufferingProgress.progress = station.bufferingProgress
|
||||
stationViewHolder.bufferingProgress.isVisible = true
|
||||
} else {
|
||||
stationViewHolder.bufferingProgress.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets the download progress view */
|
||||
private fun setDownloadProgress(stationViewHolder: StationViewHolder, station: Station) {
|
||||
if (station.downloadProgress > 0) {
|
||||
stationViewHolder.downloadProgress.progress = station.downloadProgress
|
||||
stationViewHolder.downloadProgress.isVisible = true
|
||||
} else {
|
||||
stationViewHolder.downloadProgress.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Sets the edit views */
|
||||
private fun setEditViews(stationViewHolder: StationViewHolder, station: Station) {
|
||||
stationViewHolder.stationNameEditView.setText(station.name, TextView.BufferType.EDITABLE)
|
||||
@@ -471,24 +496,25 @@ class CollectionAdapter(
|
||||
|
||||
} else if (holder is StationViewHolder) {
|
||||
// get station from position
|
||||
collection.stations[holder.bindingAdapterPosition]
|
||||
val station: Station = collection.stations[holder.bindingAdapterPosition]
|
||||
|
||||
for (data in payloads) {
|
||||
when (data as Int) {
|
||||
Keys.HOLDER_UPDATE_COVER -> {
|
||||
// todo implement
|
||||
setStationImage(holder, station)
|
||||
setStarredIcon(holder, station)
|
||||
}
|
||||
Keys.HOLDER_UPDATE_NAME -> {
|
||||
// todo implement
|
||||
setStationName(holder, station)
|
||||
}
|
||||
Keys.HOLDER_UPDATE_PLAYBACK_STATE -> {
|
||||
// todo implement
|
||||
setStationButtons(holder, station)
|
||||
}
|
||||
Keys.HOLDER_UPDATE_PLAYBACK_PROGRESS -> {
|
||||
// todo implement
|
||||
setPlaybackProgress(holder, station)
|
||||
}
|
||||
Keys.HOLDER_UPDATE_DOWNLOAD_STATE -> {
|
||||
// todo implement
|
||||
setDownloadProgress(holder, station)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,21 +601,6 @@ class CollectionAdapter(
|
||||
}
|
||||
|
||||
|
||||
// /* Initiates update of a station's information */ // todo move to CollectionHelper
|
||||
// private fun updateStation(context: Context, station: Station) {
|
||||
// if (station.radioBrowserStationUuid.isNotEmpty()) {
|
||||
// // get updated station from radio browser - results are handled by onRadioBrowserSearchResults
|
||||
// val radioBrowserSearch: RadioBrowserSearch = RadioBrowserSearch(context, this)
|
||||
// radioBrowserSearch.searchStation(context, station.radioBrowserStationUuid, Keys.SEARCH_TYPE_BY_UUID)
|
||||
// } else if (station.remoteStationLocation.isNotEmpty()) {
|
||||
// // download playlist // todo check content type detection is necessary here
|
||||
// DownloadHelper.downloadPlaylists(context, arrayOf(station.remoteStationLocation))
|
||||
// } else {
|
||||
// Log.w(TAG, "Unable to update station: ${station.name}.")
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/* Determines if position is last */
|
||||
private fun isPositionFooter(position: Int): Boolean {
|
||||
return position == collection.stations.size
|
||||
@@ -600,7 +611,7 @@ class CollectionAdapter(
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private fun updateRecyclerView(oldCollection: Collection, newCollection: Collection) {
|
||||
collection = newCollection
|
||||
if (oldCollection.stations.size == 0 && newCollection.stations.size > 0) {
|
||||
if (oldCollection.stations.isEmpty() && newCollection.stations.isNotEmpty()) {
|
||||
// data set has been initialized - redraw the whole list
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
@@ -672,6 +683,8 @@ class CollectionAdapter(
|
||||
val stationImageView: ImageView = stationCardLayout.findViewById(R.id.station_icon)
|
||||
val stationNameView: TextView = stationCardLayout.findViewById(R.id.station_name)
|
||||
val stationStarredView: ImageView = stationCardLayout.findViewById(R.id.starred_icon)
|
||||
val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress)
|
||||
val downloadProgress: ProgressBar = stationCardLayout.findViewById(R.id.download_progress)
|
||||
|
||||
// val menuButtonView: ImageView = stationCardLayout.findViewById(R.id.menu_button)
|
||||
val playButtonView: ImageView = stationCardLayout.findViewById(R.id.playback_button)
|
||||
|
||||
@@ -48,7 +48,9 @@ data class Station(
|
||||
@Expose var radioBrowserStationUuid: String = String(),
|
||||
@Expose var radioBrowserChangeUuid: String = String(),
|
||||
@Expose var bitrate: Int = 0,
|
||||
@Expose var codec: String = String()
|
||||
@Expose var codec: String = String(),
|
||||
@Expose var bufferingProgress: Int = 0,
|
||||
@Expose var downloadProgress: Int = 0
|
||||
) : Parcelable {
|
||||
|
||||
|
||||
@@ -79,6 +81,12 @@ data class Station(
|
||||
}
|
||||
|
||||
|
||||
/* Getter for media type */
|
||||
fun getMediaType(): String {
|
||||
return streamContent
|
||||
}
|
||||
|
||||
|
||||
/* Creates a deep copy of a Station */
|
||||
fun deepCopy(): Station {
|
||||
return Station(
|
||||
@@ -101,7 +109,9 @@ data class Station(
|
||||
radioBrowserStationUuid = radioBrowserStationUuid,
|
||||
radioBrowserChangeUuid = radioBrowserChangeUuid,
|
||||
bitrate = bitrate,
|
||||
codec = codec
|
||||
codec = codec,
|
||||
bufferingProgress = bufferingProgress,
|
||||
downloadProgress = downloadProgress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import androidx.media3.common.Metadata
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.extractor.metadata.icy.IcyHeaders
|
||||
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||
import androidx.media3.extractor.metadata.id3.Id3Frame
|
||||
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||
import com.michatec.radio.Keys
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -37,28 +39,51 @@ object AudioHelper {
|
||||
/* Extract audio stream metadata */
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getMetadataString(metadata: Metadata): String {
|
||||
var metadataString = String()
|
||||
var title = ""
|
||||
var artist = ""
|
||||
var album = ""
|
||||
for (i in 0 until metadata.length()) {
|
||||
// extract IceCast metadata
|
||||
when (val entry = metadata.get(i)) {
|
||||
is IcyInfo -> {
|
||||
metadataString = entry.title.toString()
|
||||
title = entry.title.toString()
|
||||
}
|
||||
|
||||
is IcyHeaders -> {
|
||||
Log.i(TAG, "icyHeaders:" + entry.name + " - " + entry.genre)
|
||||
}
|
||||
|
||||
is Id3Frame -> {
|
||||
when (entry) {
|
||||
is TextInformationFrame -> {
|
||||
when (entry.id) {
|
||||
"TIT2" -> title = entry.values.getOrNull(0) ?: "" // Title
|
||||
"TPE1" -> artist = entry.values.getOrNull(0) ?: "" // Artist
|
||||
"TALB" -> album = entry.values.getOrNull(0) ?: "" // Album
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Unhandled ID3 frame: ${entry.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unsupported metadata received (type = ${entry.javaClass.simpleName})")
|
||||
}
|
||||
}
|
||||
// TODO implement HLS metadata extraction (Id3Frame / PrivFrame)
|
||||
// https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/metadata/Metadata.Entry.html
|
||||
}
|
||||
// Build metadata string
|
||||
var metadataString = title
|
||||
if (artist.isNotEmpty() && title.isNotEmpty()) {
|
||||
metadataString = "$artist - $title"
|
||||
}
|
||||
if (album.isNotEmpty() && metadataString.isNotEmpty()) {
|
||||
metadataString += " ($album)"
|
||||
}
|
||||
// ensure a max length of the metadata string
|
||||
if (metadataString.isNotEmpty()) {
|
||||
metadataString = metadataString.substring(0, min(metadataString.length, Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY))
|
||||
metadataString = metadataString.take(min(metadataString.length, Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY))
|
||||
}
|
||||
return metadataString
|
||||
}
|
||||
|
||||
@@ -688,7 +688,6 @@ object CollectionHelper {
|
||||
|
||||
/* Creates a MediaItem with MediaMetadata for a single radio station - used to prepare player */
|
||||
fun buildMediaItem(context: Context, station: Station): MediaItem {
|
||||
// todo implement HLS MediaItems
|
||||
// put uri in RequestMetadata - credit: https://stackoverflow.com/a/70103460
|
||||
val requestMetadata = MediaItem.RequestMetadata.Builder().apply {
|
||||
setMediaUri(station.getStreamUri().toUri())
|
||||
@@ -713,7 +712,7 @@ object CollectionHelper {
|
||||
setMediaId(station.uuid)
|
||||
setRequestMetadata(requestMetadata)
|
||||
setMediaMetadata(mediaMetadata)
|
||||
//setMimeType(station.getMediaType())
|
||||
setMimeType(station.getMediaType())
|
||||
setUri(station.getStreamUri().toUri())
|
||||
}.build()
|
||||
}
|
||||
|
||||
@@ -417,16 +417,14 @@ object FileHelper {
|
||||
|
||||
/* Reads InputStream from file uri and returns it as String */
|
||||
private fun readTextFileFromFile(context: Context): String {
|
||||
// todo read https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
|
||||
// https://developer.android.com/training/secure-file-sharing/retrieve-info
|
||||
|
||||
// check if file exists
|
||||
val file = File(context.getExternalFilesDir(Keys.FOLDER_COLLECTION), Keys.COLLECTION_FILE)
|
||||
if (!file.exists() || !file.canRead()) {
|
||||
return String()
|
||||
}
|
||||
// read until last line reached
|
||||
val stream: InputStream = file.inputStream()
|
||||
val uri = Uri.fromFile(file)
|
||||
val stream: InputStream = context.contentResolver.openInputStream(uri) ?: return String()
|
||||
val reader = BufferedReader(InputStreamReader(stream))
|
||||
val builder: StringBuilder = StringBuilder()
|
||||
reader.forEachLine {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
package com.michatec.radio.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
@@ -131,6 +132,7 @@ data class LayoutHolder(var rootView: View) {
|
||||
|
||||
|
||||
/* Updates the player views */
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) {
|
||||
|
||||
// set default metadata views, when playback has stopped
|
||||
@@ -164,12 +166,20 @@ data class LayoutHolder(var rootView: View) {
|
||||
// show only the codec when the bitrate is at "0" from radio-browser.info API
|
||||
station.codec
|
||||
} else {
|
||||
val kiloBytesPerSecond = station.bitrate / 8F
|
||||
val dataRateString = if (kiloBytesPerSecond >= 1000) {
|
||||
String.format("%.2f mb/s", kiloBytesPerSecond / 1000F)
|
||||
} else {
|
||||
String.format("%.0f kb/s", kiloBytesPerSecond)
|
||||
}
|
||||
// show the bitrate and codec if the result is available in the radio-browser.info API
|
||||
buildString {
|
||||
append(station.codec)
|
||||
append(" | ")
|
||||
append(station.bitrate)
|
||||
append("kbps")
|
||||
append(" | ")
|
||||
append(dataRateString)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -31,6 +31,21 @@
|
||||
app:shapeAppearanceOverlay="@style/RoundedCorners"
|
||||
app:srcCompat="@drawable/ic_default_station_image_72dp" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/download_progress"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:progressTint="@color/player_button_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@+id/station_icon"
|
||||
app:layout_constraintStart_toStartOf="@+id/station_icon"
|
||||
app:layout_constraintTop_toTopOf="@+id/station_icon"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/change_image_view"
|
||||
android:layout_width="0dp"
|
||||
@@ -155,6 +170,21 @@
|
||||
app:layout_constraintEnd_toStartOf="@+id/save_button"
|
||||
app:layout_constraintTop_toTopOf="@+id/save_button" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/buffering_progress"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="4dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:progressTint="@color/player_button_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/default_edit_views"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<paths>
|
||||
<external-files-path
|
||||
name="my_images"
|
||||
path="Android/data/com.michatec.urlradio/files/Pictures" />
|
||||
path="Android/data/com.michatec.radio/files/Pictures" />
|
||||
<external-path
|
||||
name="external_files"
|
||||
path="." />
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
android:shortcutShortLabel="@string/shortcut_last_station_short_label">
|
||||
|
||||
<intent
|
||||
android:action="com.jamal2367.urlradio.action.START"
|
||||
android:action="com.michatec.radio.action.START"
|
||||
android:targetClass="com.michatec.radio.MainActivity"
|
||||
android:targetPackage="com.michatec.radio">
|
||||
<extra
|
||||
|
||||
Reference in New Issue
Block a user