Updated Min SDK to 26

This commit is contained in:
Nite 2025-04-04 12:55:13 +00:00
parent 53aee22794
commit 38432a3cdc
33 changed files with 91 additions and 196 deletions

1
.gitignore vendored
View File

@ -18,6 +18,7 @@ out/
# Gradle files
.gradle/
.kotlin/
build/
# Local configuration file (sdk path, etc)

View File

@ -1,5 +1,5 @@
ext.versions = [
minSdk : 21,
minSdk : 26,
targetSdk : 33,
compileSdk : 35,
]

View File

@ -227,7 +227,7 @@ class NavigationActivity : ScopeActivity() {
// Setup app shortcuts on supported devices, but not on first start, when the server
// is not configured yet.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
if (!UApp.instance!!.isFirstRun) {
ShortcutUtil.registerShortcuts(this)
}

View File

@ -114,10 +114,8 @@ private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectFileUriExposure()
detectContentUriWithoutPermission()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
detectContentUriWithoutPermission()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
detectCredentialProtectedWhileLocked()
}

View File

@ -2,7 +2,6 @@ package org.moire.ultrasonic.imageloader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import com.squareup.picasso.Picasso.LoadedFrom.DISK
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request
@ -85,12 +84,6 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
BitmapFactory.decodeFile(path, opt)
// Now set the remaining flags
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,

View File

@ -13,7 +13,6 @@ import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.KeyEvent
@ -218,10 +217,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
intent.action = Intent.ACTION_MAIN
intent.addCategory(Intent.CATEGORY_LAUNCHER)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
var pendingIntent =
PendingIntent.getActivity(context, 10, intent, flags)
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent)
@ -235,10 +232,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
)
flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = PendingIntent.FLAG_IMMUTABLE
}
// needed starting Android 12 (S = 31)
flags = PendingIntent.FLAG_IMMUTABLE
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
intent = Intent(Constants.CMD_PROCESS_KEYCODE)

View File

@ -12,7 +12,6 @@ import android.app.Service
import android.content.Intent
import android.net.wifi.WifiManager
import android.os.Binder
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
@ -509,11 +508,7 @@ class DownloadService : Service(), KoinComponent {
try {
val context = UApp.applicationContext()
val intent = Intent(context, DownloadService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
context.startForegroundService(intent)
} catch (e: IllegalStateException) {
Timber.w(e, "Failed to start download service: the app is in the background")
}

View File

@ -7,7 +7,6 @@
package org.moire.ultrasonic.service
import android.os.Build
import android.os.Bundle
import androidx.annotation.OptIn
import androidx.car.app.connection.CarConnection
@ -268,40 +267,36 @@ class MediaLibrarySessionCallback :
}
private fun configureRepeatMode(player: Player) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Timber.d("Car app library available, observing CarConnection")
Timber.d("Car app library available, observing CarConnection")
val originalRepeatMode = player.repeatMode
val originalRepeatMode = player.repeatMode
var lastCarConnectionType = -1
var lastCarConnectionType = -1
CarConnection(UApp.applicationContext()).type.observeForever {
if (lastCarConnectionType == it) {
return@observeForever
}
lastCarConnectionType = it
Timber.d("CarConnection type changed to %s", it)
when (it) {
CarConnection.CONNECTION_TYPE_PROJECTION ->
if (!customRepeatModeSet) {
Timber.d("[CarConnection] Setting repeat mode to ALL")
player.repeatMode = Player.REPEAT_MODE_ALL
customRepeatModeSet = true
}
CarConnection.CONNECTION_TYPE_NOT_CONNECTED ->
if (customRepeatModeSet) {
Timber.d("[CarConnection] Resetting repeat mode")
player.repeatMode = originalRepeatMode
customRepeatModeSet = false
}
}
CarConnection(UApp.applicationContext()).type.observeForever {
if (lastCarConnectionType == it) {
return@observeForever
}
lastCarConnectionType = it
Timber.d("CarConnection type changed to %s", it)
when (it) {
CarConnection.CONNECTION_TYPE_PROJECTION ->
if (!customRepeatModeSet) {
Timber.d("[CarConnection] Setting repeat mode to ALL")
player.repeatMode = Player.REPEAT_MODE_ALL
customRepeatModeSet = true
}
CarConnection.CONNECTION_TYPE_NOT_CONNECTED ->
if (customRepeatModeSet) {
Timber.d("[CarConnection] Resetting repeat mode")
player.repeatMode = originalRepeatMode
customRepeatModeSet = false
}
}
} else {
Timber.d("Car app library not available")
}
}

View File

@ -11,7 +11,6 @@ import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.TaskStackBuilder
@ -337,11 +336,7 @@ class PlaybackService :
private fun getPendingIntentForContent(): PendingIntent {
val intent = Intent(this, NavigationActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
var flags = FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = flags or FLAG_IMMUTABLE
}
val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
intent.action = Intent.ACTION_MAIN
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
return PendingIntent.getActivity(this, 0, intent, flags)
@ -382,12 +377,8 @@ class PlaybackService :
TaskStackBuilder.create(this@PlaybackService).run {
addNextIntent(Intent(this@PlaybackService, NavigationActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
FLAG_IMMUTABLE
} else {
0
}
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
val immutableFlag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT
getPendingIntent(0, immutableFlag)
}
val builder =
NotificationCompat.Builder(this@PlaybackService, NOTIFICATION_CHANNEL_ID)

View File

@ -8,8 +8,6 @@
package org.moire.ultrasonic.util
import android.content.Context
import android.os.Build
import android.os.Environment
import android.text.TextUtils
import android.util.Pair
import java.io.BufferedWriter
@ -259,16 +257,7 @@ object FileUtil {
get() {
// Return cached if possible
if (cachedUltrasonicDirectory != null) return cachedUltrasonicDirectory!!
@Suppress("DEPRECATION")
cachedUltrasonicDirectory = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
File(
Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic"
)
} else {
UApp.applicationContext().getExternalFilesDir(null)!!
}
cachedUltrasonicDirectory = UApp.applicationContext().getExternalFilesDir(null)!!
return cachedUltrasonicDirectory!!
}

View File

@ -10,8 +10,6 @@ package org.moire.ultrasonic.util
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.os.Build
import androidx.annotation.RequiresApi
import java.util.Locale
/**
@ -25,11 +23,7 @@ class LocaleHelper(base: Context?) : ContextWrapper(base) {
val config = context.resources.configuration
val locale = Locale.forLanguageTag(language)
Locale.setDefault(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setSystemLocale(config, locale)
} else {
setSystemLocaleLegacy(config, locale)
}
setSystemLocale(config, locale)
config.setLayoutDirection(locale)
context = context.createConfigurationContext(config)
@ -37,13 +31,7 @@ class LocaleHelper(base: Context?) : ContextWrapper(base) {
return LocaleHelper(context)
}
@Suppress("DEPRECATION")
private fun setSystemLocaleLegacy(config: Configuration, locale: Locale?) {
config.locale = locale
}
@RequiresApi(Build.VERSION_CODES.N)
fun setSystemLocale(config: Configuration, locale: Locale?) {
private fun setSystemLocale(config: Configuration, locale: Locale?) {
config.setLocale(locale)
}
}

View File

@ -11,7 +11,6 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import androidx.activity.result.contract.ActivityResultContract
import org.moire.ultrasonic.fragment.SettingsFragment
@ -19,7 +18,7 @@ import org.moire.ultrasonic.fragment.SettingsFragment
class SelectCacheActivityContract : ActivityResultContract<String?, Uri?>() {
override fun createIntent(context: Context, input: String?): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
if (Settings.cacheLocationUri != "" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Settings.cacheLocationUri != "") {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input)
}
intent.addFlags(SettingsFragment.RW_FLAG)

View File

@ -12,13 +12,10 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import org.moire.ultrasonic.R
import org.moire.ultrasonic.activity.NavigationActivity
object ShortcutUtil {
@RequiresApi(Build.VERSION_CODES.N_MR1)
fun registerShortcuts(activity: Activity) {
val shortcutIntent = Intent(activity, NavigationActivity::class.java).apply {
action = Constants.INTENT_PLAY_RANDOM_SONGS

View File

@ -303,16 +303,14 @@ object Util {
private fun isNetworkMetered(): Boolean {
val connManager = connectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val capabilities = connManager.getNetworkCapabilities(
connManager.activeNetwork
)
if (capabilities != null &&
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
) {
return false
}
val capabilities = connManager.getNetworkCapabilities(
connManager.activeNetwork
)
if (capabilities != null &&
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
) {
return false
}
return connManager.isActiveNetworkMetered
}
@ -320,21 +318,13 @@ object Util {
@Suppress("DEPRECATION")
private fun isNetworkCellular(): Boolean {
val connManager = connectivityManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connManager.activeNetwork
?: return false // Nothing connected
connManager.getNetworkInfo(network)
?: return true // Better be safe than sorry
val capabilities = connManager.getNetworkCapabilities(network)
?: return true // Better be safe than sorry
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
} else {
// if the default network is a VPN,
// this method will return the NetworkInfo for one of its underlying networks
val info = connManager.activeNetworkInfo
?: return false // Nothing connected
info.type == ConnectivityManager.TYPE_MOBILE
}
val network = connManager.activeNetwork
?: return false // Nothing connected
connManager.getNetworkInfo(network)
?: return true // Better be safe than sorry
val capabilities = connManager.getNetworkCapabilities(network)
?: return true // Better be safe than sorry
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
}
@JvmStatic
@ -502,20 +492,18 @@ object Util {
importance: Int? = null,
notificationManager: NotificationManagerCompat
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// The suggested importance of a startForeground service notification is IMPORTANCE_LOW
val channel = NotificationChannel(
id,
name,
importance ?: NotificationManager.IMPORTANCE_DEFAULT
)
// The suggested importance of a startForeground service notification is IMPORTANCE_LOW
val channel = NotificationChannel(
id,
name,
importance ?: NotificationManager.IMPORTANCE_DEFAULT
)
channel.lightColor = android.R.color.holo_blue_dark
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
channel.lightColor = android.R.color.holo_blue_dark
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(false)
notificationManager.createNotificationChannel(channel)
}
notificationManager.createNotificationChannel(channel)
}
fun ensurePermissionToPostNotification(
@ -773,10 +761,8 @@ object Util {
val intent = Intent(context, NavigationActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
intent.putExtra(Constants.INTENT_SHOW_PLAYER, true)
return PendingIntent.getActivity(context, 0, intent, flags)
}
@ -809,12 +795,7 @@ object Util {
}
fun Service.stopForegroundRemoveNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
stopForeground(STOP_FOREGROUND_REMOVE)
}
fun dumpSettingsToLog() {

View File

@ -32,6 +32,7 @@
a:gravity="bottom" >
<EditText
a:id="@+id/chat_edittext"
a:importantForAutofill="no"
a:layout_width="0dp"
a:layout_height="wrap_content"
a:layout_weight="1"

View File

@ -8,6 +8,7 @@
<EditText
a:id="@+id/save_playlist_name"
a:importantForAutofill="no"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:inputType="text"

View File

@ -27,6 +27,7 @@
<EditText
a:id="@+id/share_description"
a:importantForAutofill="no"
a:layout_width="fill_parent"
a:layout_height="50dp"
a:layout_marginTop="4dip"

View File

@ -35,6 +35,7 @@
android:layout_alignParentEnd="false"
android:ems="5"
android:gravity="left"
android:importantForAutofill="no"
android:inputType="numberSigned" />
<Spinner

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ rating.xml
~ Copyright (C) 2009-2023 Ultrasonic developers
~
~ Distributed under terms of the GNU GPLv3 license.
-->
<menu xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/popup_rate_5"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_5"
android:title="@string/menu.rating_5" />
<item android:id="@+id/popup_rate_4"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_4"
android:title="@string/menu.rating_4" />
<item android:id="@+id/popup_rate_3"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_3"
android:title="@string/menu.rating_3" />
<item android:id="@+id/popup_rate_2"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_2"
android:title="@string/menu.rating_2" />
<item android:id="@+id/popup_rate_1"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_1"
android:title="@string/menu.rating_1" />
<item android:id="@+id/popup_rate_0"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_0"
android:title="@string/menu.rating_no_rating" />
</menu>

View File

@ -10,31 +10,37 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/popup_rate_5"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_5"
android:title="@string/menu.rating_5" />
<item android:id="@+id/popup_rate_4"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_4"
android:title="@string/menu.rating_4" />
<item android:id="@+id/popup_rate_3"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_3"
android:title="@string/menu.rating_3" />
<item android:id="@+id/popup_rate_2"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_2"
android:title="@string/menu.rating_2" />
<item android:id="@+id/popup_rate_1"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_1"
android:title="@string/menu.rating_1" />
<item android:id="@+id/popup_rate_0"
android:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom"
android:icon="@drawable/rating_star_0"
android:title="@string/menu.rating_no_rating" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,8 +1,10 @@
package org.moire.ultrasonic.imageloader
import android.net.Uri
import android.os.Environment
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
import java.io.File
import java.io.IOException
import org.amshove.kluent.`should be equal to`
import org.amshove.kluent.`should not be`
@ -16,6 +18,7 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
import org.moire.ultrasonic.util.FileUtil
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
@ -55,6 +58,10 @@ class CoverArtRequestHandlerTest {
@Test
fun `Should throw IOException when request to api failed`() {
val streamResponse = StreamResponse(null, null, 500)
FileUtil.cachedUltrasonicDirectory = File(
Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic"
)
whenever(
mockApiClient.toStreamResponse(any())
@ -74,6 +81,10 @@ class CoverArtRequestHandlerTest {
apiError = null,
responseHttpCode = 200
)
FileUtil.cachedUltrasonicDirectory = File(
Environment.getExternalStorageDirectory(),
"Android/data/org.moire.ultrasonic"
)
whenever(
mockApiClient.toStreamResponse(any())