25 Commits

Author SHA1 Message Date
Michachatz
2c1913896d Merge pull request #29 from Michatec/renovate/androidx.media3-media3-datasource-okhttp-1.x
Update dependency androidx.media3:media3-datasource-okhttp to v1.9.1
2026-01-30 18:56:34 +01:00
Michachatz
026e18ae22 Merge pull request #30 from Michatec/renovate/androidx.media3-media3-exoplayer-1.x
Update dependency androidx.media3:media3-exoplayer to v1.9.1
2026-01-30 18:56:18 +01:00
renovate[bot]
3b6ebe5e1b Update dependency androidx.media3:media3-exoplayer to v1.9.1 2026-01-30 17:55:55 +00:00
Michachatz
26eab4c64c Merge pull request #31 from Michatec/renovate/androidx.media3-media3-exoplayer-hls-1.x
Update dependency androidx.media3:media3-exoplayer-hls to v1.9.1
2026-01-30 18:55:43 +01:00
renovate[bot]
75b3deb210 Update dependency androidx.media3:media3-exoplayer-hls to v1.9.1 2026-01-30 17:55:23 +00:00
renovate[bot]
96389a5c81 Update dependency androidx.media3:media3-datasource-okhttp to v1.9.1 2026-01-30 17:55:19 +00:00
Michachatz
1544c07938 Merge pull request #32 from Michatec/renovate/androidx.media3-media3-session-1.x
Update dependency androidx.media3:media3-session to v1.9.1
2026-01-30 18:54:56 +01:00
Michachatz
65e9d1fed4 Merge pull request #33 from Michatec/renovate/androidx.navigation-navigation-fragment-ktx-2.x
Update dependency androidx.navigation:navigation-fragment-ktx to v2.9.7
2026-01-30 18:54:45 +01:00
renovate[bot]
9bfc1661b8 Update dependency androidx.navigation:navigation-fragment-ktx to v2.9.7 2026-01-30 17:54:21 +00:00
Michachatz
9a379675bf Merge pull request #34 from Michatec/renovate/androidx.navigation-navigation-ui-ktx-2.x
Update dependency androidx.navigation:navigation-ui-ktx to v2.9.7
2026-01-30 18:53:49 +01:00
renovate[bot]
529e359f7f Update dependency androidx.navigation:navigation-ui-ktx to v2.9.7 2026-01-30 17:52:58 +00:00
Michachatz
fc3a1f767a Merge pull request #35 from Michatec/renovate/androidx.activity-activity-ktx-1.x
Update dependency androidx.activity:activity-ktx to v1.12.3
2026-01-30 18:52:45 +01:00
Michachatz
9197e5c2a9 Merge pull request #36 from Michatec/renovate/androidx.work-work-runtime-ktx-2.x
Update dependency androidx.work:work-runtime-ktx to v2.11.1
2026-01-30 18:52:34 +01:00
Michachatz
5920327283 Merge pull request #37 from Michatec/renovate/gradle-9.x
Update Gradle to v9.3.1
2026-01-30 18:52:22 +01:00
renovate[bot]
769cbb505f Update Gradle to v9.3.1 2026-01-29 16:34:13 +00:00
renovate[bot]
1f5d61759c Update dependency androidx.work:work-runtime-ktx to v2.11.1 2026-01-29 04:41:58 +00:00
renovate[bot]
5b1d6d96c7 Update dependency androidx.activity:activity-ktx to v1.12.3 2026-01-29 04:41:54 +00:00
renovate[bot]
606154cfb3 Update dependency androidx.media3:media3-session to v1.9.1 2026-01-26 17:46:05 +00:00
Michatec
9b5c7e3c04 - New version Initialized 2026-01-25 17:07:30 +01:00
Michatec
4466654e92 - Fix unused import 2026-01-25 16:33:45 +01:00
Michatec
b2de7bd534 - Progress Bar added
- CollectionAdapter.kt updated
- File download optimized
- Housekeeping updated
2026-01-25 16:33:17 +01:00
Michatec
d3dfcb98f9 - Layout Changes
- HLS media extraction
2026-01-25 16:08:22 +01:00
Michachatz
0adb906438 Aktualisieren von provider_paths.xml 2026-01-22 06:55:40 +01:00
Michachatz
49e63d3aaa Aktualisieren von shortcuts.xml 2026-01-22 06:55:08 +01:00
Michatec
f892a137ce - Fix .yml 2026-01-21 18:14:38 +01:00
13 changed files with 139 additions and 68 deletions

View File

@@ -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

View File

@@ -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']
}
@@ -60,17 +60,17 @@ dependencies {
// AndroidX Stuff //
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.activity:activity-ktx:1.12.2'
implementation 'androidx.activity:activity-ktx:1.12.3'
implementation 'androidx.palette:palette-ktx:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.media:media:1.7.1'
implementation 'androidx.media3:media3-exoplayer:1.9.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.9.0'
implementation 'androidx.media3:media3-session:1.9.0'
implementation 'androidx.media3:media3-datasource-okhttp:1.9.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.6'
implementation 'androidx.work:work-runtime-ktx:2.11.0'
implementation 'androidx.media3:media3-exoplayer:1.9.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.9.1'
implementation 'androidx.media3:media3-session:1.9.1'
implementation 'androidx.media3:media3-datasource-okhttp:1.9.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.7'
implementation 'androidx.work:work-runtime-ktx:2.11.1'
// Volley HTTP request //
implementation 'com.android.volley:volley:1.2.1'

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)
}
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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="." />

View File

@@ -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

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME