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

965 lines
32 KiB
Kotlin

/*
* Util.kt
* Copyright (C) 2009-2021 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.util
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
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
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
import android.os.Build
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
import android.view.KeyEvent
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.annotation.AnyRes
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
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
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
/**
* Contains various utility functions
*/
@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) {
val tmp = File(String.format(Locale.ROOT, "%s.tmp", to.path))
val input = FileInputStream(from)
val out = FileOutputStream(tmp)
try {
input.channel.transferTo(0, from.length(), out.channel)
out.close()
if (!tmp.renameTo(to)) {
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 {
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()
} 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?
if (byteCount >= KBYTE * KBYTE * KBYTE) {
return GIGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
}
// More than 1 MB?
if (byteCount >= KBYTE * KBYTE) {
return MEGA_BYTE_FORMAT.format(byteCount.toDouble() / (KBYTE * KBYTE))
}
// More than 1 KB?
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
@Suppress("ReturnCount")
fun formatLocalizedBytes(byteCount: Long, context: Context): String {
// More than 1 GB?
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!!
.format(byteCount.toDouble() / (KBYTE * KBYTE * KBYTE))
}
// More than 1 MB?
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!!
.format(byteCount.toDouble() / (KBYTE * KBYTE))
}
// More than 1 KB?
if (byteCount >= KBYTE) {
if (KILO_BYTE_LOCALIZED_FORMAT == null) {
KILO_BYTE_LOCALIZED_FORMAT =
DecimalFormat(context.resources.getString(R.string.util_bytes_format_kilobyte))
}
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.
*/
@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.
*/
@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
@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
}
/**
* Check if a usable network for downloading media is available
*
* @return Boolean
*/
@JvmStatic
fun isNetworkConnected(): Boolean {
val info = networkInfo()
val isUnmetered = info.unmetered
val wifiRequired = Settings.isWifiRequiredForDownload
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)
.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
fun getSongsFromBookmarks(bookmarks: Iterable<Bookmark?>): MusicDirectory {
val musicDirectory = MusicDirectory()
var song: MusicDirectory.Entry
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)
}
@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(
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")
else -> return // No need to broadcast.
}
context.sendBroadcast(intent)
}
@JvmStatic
@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
}
}
@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)
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
return when {
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)
}
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 +
"://" + context.resources.getResourcePackageName(drawableId) +
'/' + context.resources.getResourceTypeName(drawableId) +
'/' + context.resources.getResourceEntryName(drawableId)
)
}
@Suppress("ComplexMethod", "LongMethod")
fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry,
mediaId: String? = null,
groupNameId: Int? = null
): MediaDescriptionCompat {
val descriptionBuilder = MediaDescriptionCompat.Builder()
val artist = StringBuilder(LINE_LENGTH)
var bitRate: String? = null
val duration = song.duration
if (duration != null) {
artist.append(
String.format(Locale.ROOT, "%s ", formatTotalDuration(duration.toLong()))
)
}
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
) suffix else String.format(Locale.ROOT, "%s > %s", suffix, transcodedSuffix)
val artistName = song.artist
if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
) {
artist.append(artistName).append(" (").append(
String.format(
appContext().getString(R.string.song_details_all),
if (bitRate == null) ""
else String.format(Locale.ROOT, "%s ", bitRate),
fileFormat
)
).append(')')
} else {
artist.append(artistName)
}
}
val trackNumber = song.track ?: 0
val title = StringBuilder(LINE_LENGTH)
if (Settings.shouldShowTrackNumber && trackNumber > 0)
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),
if (bitRate == null) ""
else String.format(Locale.ROOT, "%s ", bitRate),
fileFormat
)
).append(')')
}
if (groupNameId != null)
descriptionBuilder.setExtras(
Bundle().apply {
putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
appContext().getString(groupNameId)
)
}
)
descriptionBuilder.setTitle(title)
descriptionBuilder.setSubtitle(artist)
descriptionBuilder.setMediaId(mediaId)
return descriptionBuilder.build()
}
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
}
data class NetworkInfo(
var connected: Boolean = false,
var unmetered: Boolean = false
)
}