ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Util.kt

965 lines
32 KiB
Kotlin
Raw Normal View History

/*
2021-07-18 13:17:29 +02:00
* Util.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
2021-07-18 13:17:29 +02:00
package org.moire.ultrasonic.util
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
2021-07-18 11:33:39 +02:00
import android.app.PendingIntent
import android.content.ContentResolver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
2021-11-01 15:09:53 +01:00
import android.net.Network
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
import android.net.Uri
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
2021-11-01 15:09:53 +01:00
import android.os.Build
2021-07-18 11:33:39 +02:00
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
import android.support.v4.media.MediaDescriptionCompat
import android.text.TextUtils
import android.util.TypedValue
import android.view.Gravity
2021-07-18 11:33:39 +02:00
import android.view.KeyEvent
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.AnyRes
2021-07-18 11:33:39 +02:00
import androidx.media.utils.MediaConstants
import java.io.Closeable
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.security.MessageDigest
import java.text.DecimalFormat
2021-07-18 13:17:29 +02:00
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
2021-07-18 13:17:29 +02:00
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.service.DownloadFile
import timber.log.Timber
private const val LINE_LENGTH = 60
private const val DEGRADE_PRECISION_AFTER = 10
private const val MINUTES_IN_HOUR = 60
private const val KBYTE = 1024
/**
2021-07-18 13:17:29 +02:00
* Contains various utility functions
*/
2021-07-18 13:17:29 +02:00
@Suppress("TooManyFunctions", "LargeClass")
object Util {
private val GIGA_BYTE_FORMAT = DecimalFormat("0.00 GB")
private val MEGA_BYTE_FORMAT = DecimalFormat("0.00 MB")
private val KILO_BYTE_FORMAT = DecimalFormat("0 KB")
private var GIGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private var BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private const val EVENT_META_CHANGED = "org.moire.ultrasonic.EVENT_META_CHANGED"
private const val EVENT_PLAYSTATE_CHANGED = "org.moire.ultrasonic.EVENT_PLAYSTATE_CHANGED"
private const val CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged"
private const val CM_AVRCP_PLAYBACK_COMPLETE = "com.android.music.playbackcomplete"
private const val CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged"
// Used by hexEncode()
private val HEX_DIGITS =
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
private var toast: Toast? = null
// Retrieves an instance of the application Context
fun appContext(): Context {
return applicationContext()
}
@JvmStatic
fun applyTheme(context: Context?) {
when (Settings.theme.lowercase()) {
Constants.PREFERENCES_KEY_THEME_DARK,
"fullscreen" -> {
context!!.setTheme(R.style.UltrasonicTheme)
}
Constants.PREFERENCES_KEY_THEME_BLACK -> {
context!!.setTheme(R.style.UltrasonicTheme_Black)
}
Constants.PREFERENCES_KEY_THEME_LIGHT,
"fullscreenlight" -> {
context!!.setTheme(R.style.UltrasonicTheme_Light)
}
}
}
@Throws(IOException::class)
fun atomicCopy(from: File, to: File) {
2021-07-18 13:17:29 +02:00
val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
val input = FileInputStream(from)
val out = FileOutputStream(tmp)
try {
2021-07-18 13:17:29 +02:00
input.channel.transferTo(0, from.length(), out.channel)
out.close()
if (!tmp.renameTo(to)) {
2021-07-18 13:17:29 +02:00
throw IOException(
String.format(Locale.ROOT, "Failed to rename %s to %s", tmp, to)
)
}
Timber.i("Copied %s to %s", from, to)
} catch (x: IOException) {
close(out)
delete(to)
throw x
} finally {
2021-07-18 13:17:29 +02:00
close(input)
close(out)
delete(tmp)
}
}
@JvmStatic
@Throws(IOException::class)
fun renameFile(from: File, to: File) {
if (from.renameTo(to)) {
Timber.i("Renamed %s to %s", from, to)
} else {
atomicCopy(from, to)
}
}
@JvmStatic
fun close(closeable: Closeable?) {
try {
closeable?.close()
2021-07-18 13:17:29 +02:00
} catch (_: Throwable) {
// Ignored
}
}
@JvmStatic
fun delete(file: File?): Boolean {
if (file != null && file.exists()) {
if (!file.delete()) {
Timber.w("Failed to delete file %s", file)
return false
}
Timber.i("Deleted file %s", file)
}
return true
}
@JvmStatic
@JvmOverloads
fun toast(context: Context?, messageId: Int, shortDuration: Boolean = true) {
toast(context, context!!.getString(messageId), shortDuration)
}
@JvmStatic
fun toast(context: Context?, message: CharSequence?) {
toast(context, message, true)
}
@JvmStatic
@SuppressLint("ShowToast") // Invalid warning
fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) {
if (toast == null) {
toast = Toast.makeText(
context,
message,
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
)
toast!!.setGravity(Gravity.CENTER, 0, 0)
} else {
toast!!.setText(message)
toast!!.duration =
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
}
toast!!.show()
}
/**
* Formats an Int to a percentage string
* For instance:
*
* * `format(99)` returns *"99 %"*.
*
*
* @param percent The percent as a range from 0 - 100
* @return The formatted string.
*/
@Synchronized
fun formatPercentage(percent: Int): String {
return min(max(percent, 0), 100).toString() + " %"
}
/**
* Converts a byte-count to a formatted string suitable for display to the user.
* For instance:
*
* * `format(918)` returns *"918 B"*.
* * `format(98765)` returns *"96 KB"*.
* * `format(1238476)` returns *"1.2 MB"*.
*
* This method assumes that 1 KB is 1024 bytes.
* To get a localized string, please use formatLocalizedBytes instead.
*
* @param byteCount The number of bytes.
* @return The formatted string.
*/
@JvmStatic
@Synchronized
fun formatBytes(byteCount: Long): String {
// More than 1 GB?
2021-07-18 13:17:29 +02:00
if (byteCount >= KBYTE * KBYTE * KBYTE) {
return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
}
// More than 1 MB?
2021-07-18 13:17:29 +02:00
if (byteCount >= KBYTE * KBYTE) {
return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE))
}
// More than 1 KB?
2021-07-18 13:17:29 +02:00
return if (byteCount >= KBYTE) {
KILO_BYTE_FORMAT.format(byteCount.toDouble() / KBYTE)
} else "$byteCount B"
}
/**
* Converts a byte-count to a formatted string suitable for display to the user.
* For instance:
*
* * `format(918)` returns *"918 B"*.
* * `format(98765)` returns *"96 KB"*.
* * `format(1238476)` returns *"1.2 MB"*.
*
* This method assumes that 1 KB is 1024 bytes.
* This version of the method returns a localized string.
*
* @param byteCount The number of bytes.
* @return The formatted string.
*/
@Synchronized
2021-07-18 13:17:29 +02:00
@Suppress("ReturnCount")
fun formatLocalizedBytes(byteCount: Long, context: Context): String {
// More than 1 GB?
2021-07-18 13:17:29 +02:00
if (byteCount >= KBYTE * KBYTE * KBYTE) {
if (GIGA_BYTE_LOCALIZED_FORMAT == null) {
GIGA_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_gigabyte))
}
return GIGA_BYTE_LOCALIZED_FORMAT!!
2021-07-18 13:17:29 +02:00
.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
}
// More than 1 MB?
2021-07-18 13:17:29 +02:00
if (byteCount >= KBYTE * KBYTE) {
if (MEGA_BYTE_LOCALIZED_FORMAT == null) {
MEGA_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_megabyte))
}
return MEGA_BYTE_LOCALIZED_FORMAT!!
2021-07-18 13:17:29 +02:00
.format(byteCount.toDouble() / (KBYTE * KBYTE))
}
// More than 1 KB?
2021-07-18 13:17:29 +02:00
if (byteCount >= KBYTE) {
if (KILO_BYTE_LOCALIZED_FORMAT == null) {
KILO_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_kilobyte))
}
2021-07-18 13:17:29 +02:00
return KILO_BYTE_LOCALIZED_FORMAT!!.format(byteCount.toDouble() / KBYTE)
}
if (BYTE_LOCALIZED_FORMAT == null) {
BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_byte))
}
return BYTE_LOCALIZED_FORMAT!!.format(byteCount.toDouble())
}
@Suppress("SuspiciousEqualsCombination")
fun equals(object1: Any?, object2: Any?): Boolean {
return object1 === object2 || !(object1 == null || object2 == null) && object1 == object2
}
/**
* Encodes the given string by using the hexadecimal representation of its UTF-8 bytes.
*
* @param s The string to encode.
* @return The encoded string.
*/
2021-07-18 13:17:29 +02:00
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
fun utf8HexEncode(s: String?): String? {
if (s == null) {
return null
}
val utf8: ByteArray = try {
s.toByteArray(charset(Constants.UTF_8))
} catch (x: UnsupportedEncodingException) {
throw RuntimeException(x)
}
return hexEncode(utf8)
}
/**
* Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
* The returned array will be double the length of the passed array, as it takes two characters to represent any
* given byte.
*
* @param data Bytes to convert to hexadecimal characters.
* @return A string containing hexadecimal characters.
*/
2021-07-18 13:17:29 +02:00
@Suppress("MagicNumber")
fun hexEncode(data: ByteArray): String {
val length = data.size
val out = CharArray(length shl 1)
var j = 0
// two characters form the hex value.
for (aData in data) {
out[j++] = HEX_DIGITS[0xF0 and aData.toInt() ushr 4]
out[j++] = HEX_DIGITS[0x0F and aData.toInt()]
}
return String(out)
}
/**
* Calculates the MD5 digest and returns the value as a 32 character hex string.
*
* @param s Data to digest.
* @return MD5 digest as a hex string.
*/
@JvmStatic
2021-07-18 13:17:29 +02:00
@Suppress("TooGenericExceptionThrown", "TooGenericExceptionCaught")
fun md5Hex(s: String?): String? {
return if (s == null) {
null
} else try {
val md5 = MessageDigest.getInstance("MD5")
hexEncode(md5.digest(s.toByteArray(charset(Constants.UTF_8))))
} catch (x: Exception) {
throw RuntimeException(x.message, x)
}
}
@JvmStatic
fun getGrandparent(path: String?): String? {
// Find the top level folder, assume it is the album artist
if (path != null) {
val slashIndex = path.indexOf('/')
if (slashIndex > 0) {
return path.substring(0, slashIndex)
}
}
return null
}
2021-11-01 15:09:53 +01:00
/**
* Check if a usable network for downloading media is available
*
* @return Boolean
*/
@JvmStatic
fun isNetworkConnected(): Boolean {
2021-11-01 15:09:53 +01:00
val info = networkInfo()
val isUnmetered = info.unmetered
val wifiRequired = Settings.isWifiRequiredForDownload
2021-11-01 15:09:53 +01:00
return info.connected && (!wifiRequired || isUnmetered)
}
/**
* Query connectivity status
*
* @return NetworkInfo object
*/
@Suppress("DEPRECATION")
fun networkInfo(): NetworkInfo {
val manager = getConnectivityManager()
val info = NetworkInfo()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network: Network? = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null) {
info.unmetered = capabilities.hasCapability(NET_CAPABILITY_NOT_METERED)
info.connected = capabilities.hasCapability(NET_CAPABILITY_INTERNET)
}
} else {
val networkInfo = manager.activeNetworkInfo
if (networkInfo != null) {
info.unmetered = networkInfo.type == ConnectivityManager.TYPE_WIFI
info.connected = networkInfo.isConnected
}
}
return info
}
@JvmStatic
fun isExternalStoragePresent(): Boolean =
Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
// The AlertDialog requires an Activity context, app context is not enough
// See https://stackoverflow.com/questions/5436822/
fun createDialog(
context: Context?,
icon: Int = android.R.drawable.ic_dialog_info,
title: String,
message: String?
): AlertDialog.Builder {
return AlertDialog.Builder(context)
.setIcon(icon)
.setTitle(title)
.setMessage(message)
2021-07-18 13:17:29 +02:00
.setPositiveButton(R.string.common_ok) {
dialog: DialogInterface,
_: Int ->
dialog.dismiss()
}
}
fun showDialog(
context: Context,
icon: Int = android.R.drawable.ic_dialog_info,
titleId: Int,
message: String?
) {
createDialog(context, icon, context.getString(titleId, ""), message).show()
}
@JvmStatic
fun sleepQuietly(millis: Long) {
try {
Thread.sleep(millis)
} catch (x: InterruptedException) {
Timber.w(x, "Interrupted from sleep.")
}
}
@JvmStatic
fun getDrawableFromAttribute(context: Context?, attr: Int): Drawable {
val attrs = intArrayOf(attr)
val ta = context!!.obtainStyledAttributes(attrs)
val drawableFromTheme: Drawable? = ta.getDrawable(0)
ta.recycle()
return drawableFromTheme!!
}
fun createDrawableFromBitmap(context: Context, bitmap: Bitmap?): Drawable {
return BitmapDrawable(context.resources, bitmap)
}
fun createBitmapFromDrawable(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun createWifiLock(tag: String?): WifiLock {
val wm =
appContext().applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
return wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, tag)
}
fun getScaledHeight(height: Double, width: Double, newWidth: Int): Int {
// Try to keep correct aspect ratio of the original image, do not force a square
val aspectRatio = height / width
// Assume the size given refers to the width of the image, so calculate the new height using
// the previously determined aspect ratio
return (newWidth * aspectRatio).roundToInt()
}
private fun getScaledHeight(bitmap: Bitmap, width: Int): Int {
return getScaledHeight(bitmap.height.toDouble(), bitmap.width.toDouble(), width)
}
fun scaleBitmap(bitmap: Bitmap?, size: Int): Bitmap? {
return if (bitmap == null) null else Bitmap.createScaledBitmap(
bitmap,
size,
getScaledHeight(bitmap, size),
true
)
}
fun getSongsFromSearchResult(searchResult: SearchResult): MusicDirectory {
val musicDirectory = MusicDirectory()
for (entry in searchResult.songs) {
musicDirectory.addChild(entry)
}
return musicDirectory
}
@JvmStatic
2021-07-18 11:33:39 +02:00
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
val musicDirectory = MusicDirectory()
var song: MusicDirectory.Entry
2021-07-18 11:33:39 +02:00
for (bookmark in bookmarks) {
if (bookmark == null) continue
song = bookmark.entry
song.bookmarkPosition = bookmark.position
musicDirectory.addChild(song)
}
return musicDirectory
}
/**
*
* Broadcasts the given song info as the new song being played.
*/
fun broadcastNewTrackInfo(context: Context, song: MusicDirectory.Entry?) {
val intent = Intent(EVENT_META_CHANGED)
if (song != null) {
intent.putExtra("title", song.title)
intent.putExtra("artist", song.artist)
intent.putExtra("album", song.album)
val albumArtFile = FileUtil.getAlbumArtFile(song)
intent.putExtra("coverart", albumArtFile.absolutePath)
} else {
intent.putExtra("title", "")
intent.putExtra("artist", "")
intent.putExtra("album", "")
intent.putExtra("coverart", "")
}
context.sendBroadcast(intent)
}
fun broadcastA2dpMetaDataChange(
context: Context,
playerPosition: Int,
currentPlaying: DownloadFile?,
listSize: Int,
id: Int
) {
if (!Settings.shouldSendBluetoothNotifications) return
var song: MusicDirectory.Entry? = null
val avrcpIntent = Intent(CM_AVRCP_METADATA_CHANGED)
if (currentPlaying != null) song = currentPlaying.song
fillIntent(avrcpIntent, song, playerPosition, id, listSize)
context.sendBroadcast(avrcpIntent)
}
2021-07-18 13:17:29 +02:00
@Suppress("LongParameterList")
fun broadcastA2dpPlayStatusChange(
context: Context,
state: PlayerState?,
newSong: MusicDirectory.Entry?,
listSize: Int,
id: Int,
playerPosition: Int
) {
if (!Settings.shouldSendBluetoothNotifications) return
if (newSong != null) {
val avrcpIntent = Intent(
if (state == PlayerState.COMPLETED) CM_AVRCP_PLAYBACK_COMPLETE
else CM_AVRCP_PLAYSTATE_CHANGED
)
fillIntent(avrcpIntent, newSong, playerPosition, id, listSize)
if (state != PlayerState.COMPLETED) {
when (state) {
PlayerState.STARTED -> avrcpIntent.putExtra("playing", true)
PlayerState.STOPPED,
PlayerState.PAUSED -> avrcpIntent.putExtra("playing", false)
else -> return // No need to broadcast.
}
}
context.sendBroadcast(avrcpIntent)
}
}
private fun fillIntent(
2021-10-07 18:02:23 +02:00
intent: Intent,
song: MusicDirectory.Entry?,
playerPosition: Int,
id: Int,
listSize: Int
) {
if (song == null) {
intent.putExtra("track", "")
intent.putExtra("track_name", "")
intent.putExtra("artist", "")
intent.putExtra("artist_name", "")
intent.putExtra("album", "")
intent.putExtra("album_name", "")
intent.putExtra("album_artist", "")
intent.putExtra("album_artist_name", "")
if (Settings.shouldSendBluetoothAlbumArt) {
intent.putExtra("coverart", null as Parcelable?)
intent.putExtra("cover", null as Parcelable?)
}
intent.putExtra("ListSize", 0.toLong())
intent.putExtra("id", 0.toLong())
intent.putExtra("duration", 0.toLong())
intent.putExtra("position", 0.toLong())
} else {
val title = song.title
val artist = song.artist
val album = song.album
val duration = song.duration
intent.putExtra("track", title)
intent.putExtra("track_name", title)
intent.putExtra("artist", artist)
intent.putExtra("artist_name", artist)
intent.putExtra("album", album)
intent.putExtra("album_name", album)
intent.putExtra("album_artist", artist)
intent.putExtra("album_artist_name", artist)
if (Settings.shouldSendBluetoothAlbumArt) {
val albumArtFile = FileUtil.getAlbumArtFile(song)
intent.putExtra("coverart", albumArtFile.absolutePath)
intent.putExtra("cover", albumArtFile.absolutePath)
}
intent.putExtra("position", playerPosition.toLong())
intent.putExtra("id", id.toLong())
intent.putExtra("ListSize", listSize.toLong())
if (duration != null) {
intent.putExtra("duration", duration.toLong())
}
}
}
/**
*
* Broadcasts the given player state as the one being set.
*/
fun broadcastPlaybackStatusChange(context: Context, state: PlayerState?) {
val intent = Intent(EVENT_PLAYSTATE_CHANGED)
when (state) {
PlayerState.STARTED -> intent.putExtra("state", "play")
PlayerState.STOPPED -> intent.putExtra("state", "stop")
PlayerState.PAUSED -> intent.putExtra("state", "pause")
PlayerState.COMPLETED -> intent.putExtra("state", "complete")
2021-07-18 13:17:29 +02:00
else -> return // No need to broadcast.
}
context.sendBroadcast(intent)
}
@JvmStatic
2021-07-18 13:17:29 +02:00
@Suppress("MagicNumber")
fun getNotificationImageSize(context: Context): Int {
val metrics = context.resources.displayMetrics
val imageSizeLarge =
min(metrics.widthPixels, metrics.heightPixels).toFloat().roundToInt()
return when {
imageSizeLarge <= 480 -> {
64
}
imageSizeLarge <= 768 -> 128
else -> 256
}
}
2021-07-18 13:17:29 +02:00
@Suppress("MagicNumber")
fun getAlbumImageSize(context: Context?): Int {
val metrics = context!!.resources.displayMetrics
val imageSizeLarge =
min(metrics.widthPixels, metrics.heightPixels).toFloat().roundToInt()
return when {
imageSizeLarge <= 480 -> {
128
}
imageSizeLarge <= 768 -> 256
else -> 512
}
}
fun getMinDisplayMetric(): Int {
val metrics = appContext().resources.displayMetrics
return min(metrics.widthPixels, metrics.heightPixels)
}
fun getMaxDisplayMetric(): Int {
val metrics = appContext().resources.displayMetrics
return max(metrics.widthPixels, metrics.heightPixels)
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// Raw height and width of image
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
// Calculate ratios of height and width to requested height and
// width
val heightRatio = (height.toFloat() / reqHeight.toFloat()).roundToInt()
val widthRatio = (width.toFloat() / reqWidth.toFloat()).roundToInt()
// Choose the smallest ratio as inSampleSize value, this will
// guarantee
// a final image with both dimensions larger than or equal to the
// requested height and width.
inSampleSize = min(heightRatio, widthRatio)
}
return inSampleSize
}
@JvmStatic
fun isNullOrWhiteSpace(string: String?): Boolean {
return string == null || string.isEmpty() || string.trim { it <= ' ' }.isEmpty()
}
@JvmOverloads
fun formatTotalDuration(totalDuration: Long, inMilliseconds: Boolean = false): String {
var millis = totalDuration
if (!inMilliseconds) {
millis = totalDuration * 1000
}
val hours = TimeUnit.MILLISECONDS.toHours(millis)
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours)
2021-07-18 13:17:29 +02:00
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
return when {
2021-07-18 13:17:29 +02:00
hours >= DEGRADE_PRECISION_AFTER -> {
String.format(
Locale.getDefault(),
"%02d:%02d:%02d",
hours,
minutes,
seconds
)
}
hours > 0 -> {
String.format(Locale.getDefault(), "%d:%02d:%02d", hours, minutes, seconds)
}
2021-07-18 13:17:29 +02:00
minutes >= DEGRADE_PRECISION_AFTER -> {
String.format(Locale.getDefault(), "%02d:%02d", minutes, seconds)
}
minutes > 0 -> String.format(
Locale.getDefault(),
"%d:%02d",
minutes,
seconds
)
else -> String.format(Locale.getDefault(), "0:%02d", seconds)
}
}
@JvmStatic
fun getVersionName(context: Context): String? {
var versionName: String? = null
val pm = context.packageManager
if (pm != null) {
val packageName = context.packageName
try {
versionName = pm.getPackageInfo(packageName, 0).versionName
} catch (ignored: PackageManager.NameNotFoundException) {
}
}
return versionName
}
fun getVersionCode(context: Context): Int {
var versionCode = 0
val pm = context.packageManager
if (pm != null) {
val packageName = context.packageName
try {
versionCode = pm.getPackageInfo(packageName, 0).versionCode
} catch (ignored: PackageManager.NameNotFoundException) {
}
}
return versionCode
}
@JvmStatic
fun scanMedia(file: File?) {
val uri = Uri.fromFile(file)
val scanFileIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)
appContext().sendBroadcast(scanFileIntent)
}
fun getResourceFromAttribute(context: Context, resId: Int): Int {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(resId, typedValue, true)
return typedValue.resourceId
}
fun isFirstRun(): Boolean {
if (Settings.firstRunExecuted) return false
Settings.firstRunExecuted = true
return true
}
fun hideKeyboard(activity: Activity?) {
val inputManager =
activity!!.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusedView = activity.currentFocus
if (currentFocusedView != null) {
inputManager.hideSoftInputFromWindow(
currentFocusedView.windowToken,
InputMethodManager.HIDE_NOT_ALWAYS
)
}
}
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE +
2021-07-18 13:17:29 +02:00
"://" + context.resources.getResourcePackageName(drawableId) +
'/' + context.resources.getResourceTypeName(drawableId) +
'/' + context.resources.getResourceEntryName(drawableId)
)
}
2021-07-18 13:23:20 +02:00
@Suppress("ComplexMethod", "LongMethod")
2021-07-18 11:33:39 +02:00
fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry,
mediaId: String? = null,
groupNameId: Int? = null
): MediaDescriptionCompat {
val descriptionBuilder = MediaDescriptionCompat.Builder()
2021-07-18 13:17:29 +02:00
val artist = StringBuilder(LINE_LENGTH)
var bitRate: String? = null
val duration = song.duration
if (duration != null) {
2021-07-18 13:17:29 +02:00
artist.append(
String.format(Locale.ROOT, "%s ", formatTotalDuration(duration.toLong()))
)
}
2021-07-18 11:33:39 +02:00
if (song.bitRate != null && song.bitRate!! > 0)
bitRate = String.format(
appContext().getString(R.string.song_details_kbps), song.bitRate
)
val fileFormat: String?
val suffix = song.suffix
val transcodedSuffix = song.transcodedSuffix
fileFormat = if (
TextUtils.isEmpty(transcodedSuffix) || transcodedSuffix == suffix || song.isVideo
2021-07-18 13:17:29 +02:00
) suffix else String.format(Locale.ROOT, "%s > %s", suffix, transcodedSuffix)
val artistName = song.artist
if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && (
2021-07-18 13:17:29 +02:00
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
) {
artist.append(artistName).append(" (").append(
String.format(
appContext().getString(R.string.song_details_all),
2021-07-18 13:17:29 +02:00
if (bitRate == null) ""
else String.format(Locale.ROOT, "%s ", bitRate),
fileFormat
)
).append(')')
} else {
artist.append(artistName)
}
}
val trackNumber = song.track ?: 0
2021-07-18 13:17:29 +02:00
val title = StringBuilder(LINE_LENGTH)
if (Settings.shouldShowTrackNumber && trackNumber > 0)
2021-07-18 13:17:29 +02:00
title.append(String.format(Locale.ROOT, "%02d - ", trackNumber))
title.append(song.title)
if (song.isVideo && Settings.shouldDisplayBitrateWithArtist) {
title.append(" (").append(
String.format(
appContext().getString(R.string.song_details_all),
2021-07-18 13:17:29 +02:00
if (bitRate == null) ""
else String.format(Locale.ROOT, "%s ", bitRate),
fileFormat
)
).append(')')
}
2021-07-18 11:33:39 +02:00
if (groupNameId != null)
2021-07-18 13:17:29 +02:00
descriptionBuilder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
)
}
)
2021-07-18 11:33:39 +02:00
descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(artist)
descriptionBuilder.setMediaId(mediaId)
return descriptionBuilder.build()
}
2021-07-18 11:33:39 +02:00
fun getPendingIntentForMediaAction(
context: Context,
keycode: Int,
requestCode: Int
): PendingIntent {
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
val flags = PendingIntent.FLAG_UPDATE_CURRENT
intent.setPackage(context.packageName)
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
}
fun getConnectivityManager(): ConnectivityManager {
val context = appContext()
return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}
2021-11-01 15:09:53 +01:00
data class NetworkInfo(
var connected: Boolean = false,
var unmetered: Boolean = false
)
2021-07-18 13:17:29 +02:00
}