This commit is contained in:
ocean 2024-05-28 15:43:23 +08:00
parent df24548018
commit 40fb451fa4
28 changed files with 2328 additions and 66 deletions

View File

@ -14,8 +14,8 @@ android {
applicationId = "relax.offline.mp3.music" applicationId = "relax.offline.mp3.music"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 3
versionName = "1.0.1" versionName = "1.0.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@ -30,13 +30,13 @@ android {
"proguard-rules.pro" "proguard-rules.pro"
) )
} }
// debug { debug {
// isMinifyEnabled = true isMinifyEnabled = true
// proguardFiles( proguardFiles(
// getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
// "proguard-rules.pro" "proguard-rules.pro"
// ) )
// } }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
@ -60,27 +60,21 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.media3:media3-session:1.3.1") implementation("androidx.media3:media3-session:1.3.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
// implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("androidx.room:room-ktx:2.6.1") implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-runtime:2.6.1")
//noinspection KaptUsageInsteadOfKsp //noinspection KaptUsageInsteadOfKsp
kapt("androidx.room:room-compiler:2.6.1") kapt("androidx.room:room-compiler:2.6.1")
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
implementation("androidx.media3:media3-exoplayer:1.3.1") implementation("androidx.media3:media3-exoplayer:1.3.1")
implementation("androidx.media3:media3-exoplayer-dash:1.3.1") implementation("androidx.media3:media3-exoplayer-dash:1.3.1")
implementation("androidx.media3:media3-ui:1.3.1") implementation("androidx.media3:media3-ui:1.3.1")
implementation("androidx.media3:media3-common:1.3.1") implementation("androidx.media3:media3-common:1.3.1")
// implementation("com.android.tools.compose:compose-preview-renderer:0.0.1-alpha01")
// implementation("org.chromium.net:cronet-api:119.6045.31")
implementation("androidx.media3:media3-datasource-cronet:1.3.1") implementation("androidx.media3:media3-datasource-cronet:1.3.1")
implementation("io.coil-kt:coil-compose:2.6.0") implementation("io.coil-kt:coil-compose:2.6.0")
@ -92,7 +86,15 @@ dependencies {
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
implementation("org.brotli:dec:0.1.2") implementation("org.brotli:dec:0.1.2")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("com.geyifeng.immersionbar:immersionbar:3.2.2")
implementation("com.geyifeng.immersionbar:immersionbar-ktx:3.2.2")
implementation("com.github.lihangleo2:ShadowLayout:3.4.0")
implementation("com.google.android.flexbox:flexbox:3.0.0") implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation("io.github.scwang90:refresh-layout-kernel:2.1.0") implementation("io.github.scwang90:refresh-layout-kernel:2.1.0")
implementation("io.github.scwang90:refresh-footer-ball:2.1.0") implementation("io.github.scwang90:refresh-footer-ball:2.1.0")
implementation ("com.google.code.gson:gson:2.10.1")
//google
// implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
} }

View File

@ -20,4 +20,28 @@
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-dontwarn org.slf4j.impl.StaticLoggerBinder -dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn java.lang.management.ManagementFactory
-dontwarn java.lang.management.RuntimeMXBean
-dontwarn org.slf4j.impl.StaticMDCBinder
-dontwarn org.slf4j.impl.StaticMarkerBinder
# Gson 的混淆规则
# 保持 Gson 所使用的类及其字段名称
-keep class com.google.gson.** { *; }
-keep class com.google.gson.stream.** { *; }
# 保持使用 @Expose 注解的字段
-keepattributes *Annotation*
# 保持被序列化/反序列化的类(确保您的模型类不会被混淆)
-keep class your.package.name.** { *; }
# 保持使用 @SerializedName 注解的字段
-keep class ** {
@com.google.gson.annotations.SerializedName <fields>;
}
# 保持 Gson TypeAdapter
-keep class * extends com.google.gson.TypeAdapter {
*;
}

View File

@ -7,13 +7,16 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<application <application
android:name="relax.offline.music.App" android:name="relax.offline.music.App"

View File

@ -20,6 +20,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import relax.offline.music.database.AppFavoriteDBManager import relax.offline.music.database.AppFavoriteDBManager
import relax.offline.music.http.CommonIpInfoUtil
import relax.offline.music.http.UploadEventName
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
@ -119,6 +121,8 @@ class App : Application() {
initImportAudio() initImportAudio()
CacheManager.initializeCaches(this) CacheManager.initializeCaches(this)
DownloadUtil.getDownloadManager(this) DownloadUtil.getDownloadManager(this)
CommonIpInfoUtil.shared.initIPInfo()
UploadEventName.shared.init(this)
} }
} }

View File

@ -6,14 +6,14 @@ import android.os.CountDownTimer
import com.gyf.immersionbar.ktx.immersionBar import com.gyf.immersionbar.ktx.immersionBar
import relax.offline.music.databinding.ActivityLaunchBinding import relax.offline.music.databinding.ActivityLaunchBinding
class LaunchActivity : BaseActivity() { class LaunchActivity : MoBaseActivity() {
private lateinit var binding: ActivityLaunchBinding private lateinit var binding: ActivityLaunchBinding
private val totalTime = 3000L // 5秒 private val totalTime = 3000L // 5秒
private val interval = 50L // 更新间隔,毫秒 private val interval = 50L // 更新间隔,毫秒
private val steps = totalTime / interval private val steps = totalTime / interval
private val progressPerStep = 100f / steps.toFloat() private val progressPerStep = 100f / steps.toFloat()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) override suspend fun main() {
binding = ActivityLaunchBinding.inflate(layoutInflater) binding = ActivityLaunchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initTimer() initTimer()
@ -41,7 +41,11 @@ class LaunchActivity : BaseActivity() {
} }
private fun toMainActivity() { private fun toMainActivity() {
startActivity(Intent(this, PrimaryActivity::class.java)) if (!withPermission()) {
startActivity(Intent(this, MainActivity::class.java))
} else {
startActivity(Intent(this, PrimaryActivity::class.java))
}
finish() finish()
} }
} }

View File

@ -1,6 +1,7 @@
package relax.offline.music.activity package relax.offline.music.activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
@ -19,34 +20,29 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import relax.offline.music.R
import relax.offline.music.media.MediaControllerManager
import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag
import relax.offline.music.view.MusicPlayerView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import relax.offline.music.App import relax.offline.music.App
import relax.offline.music.R
import relax.offline.music.bean.FavoriteBean import relax.offline.music.bean.FavoriteBean
import relax.offline.music.bean.OfflineBean import relax.offline.music.bean.OfflineBean
import relax.offline.music.innertube.Innertube import relax.offline.music.innertube.Innertube
import relax.offline.music.util.FileSizeConverter import relax.offline.music.media.MediaControllerManager
import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag
import relax.offline.music.http.getCountryCode
import relax.offline.music.view.MusicPlayerView
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(), abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope(),
LifecycleOwner { LifecycleOwner {
private var playerListener: Player.Listener? = null private var playerListener: Player.Listener? = null
private var downloadManagerListener: DownloadManager.Listener? = null
enum class Event { enum class Event {
ActivityStart, ActivityStart,
@ -240,7 +236,7 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
isOffline = true isOffline = true
) )
LogTag.LogD(Innertube.TAG, "insertOfflineBean bean->${bean}") LogTag.LogD(Innertube.TAG, "insertOfflineBean bean->${bean}")
relax.offline.music.App.appOfflineDBManager.insertOfflineBean(bean) App.appOfflineDBManager.insertOfflineBean(bean)
} }
suspend fun insertFavoriteData(mediaItem: MediaItem) { suspend fun insertFavoriteData(mediaItem: MediaItem) {
@ -254,4 +250,46 @@ abstract class MoBaseActivity : AppCompatActivity(), CoroutineScope by MainScope
LogTag.LogD(Innertube.TAG, "insertFavoriteBean bean->${bean}") LogTag.LogD(Innertube.TAG, "insertFavoriteBean bean->${bean}")
App.appFavoriteDBManager.insertFavoriteBean(bean) App.appFavoriteDBManager.insertFavoriteBean(bean)
} }
fun withPermission(): Boolean {
// 不允许的国家代码
val restrictedCountries = setOf("CN", "HK", "TW", "JP", "KR", "GB", "CH", "BE", "MO", "SG")
// 检查是否包含当前的国家代码
if (appStore.ipCountryCode in restrictedCountries) {
return false
}
// 如果不在受限国家代码中,则继续其他检查
return withIso()
}
private fun withIso(): Boolean {
//460 || 461 China (People's Republic of)
//454 "Hong Kong, China"
//466 "Taiwan, China"
//440 || 441 Japan
//450 Korea (Republic of)
//234 || 235 United Kingdom of Great Britain and Northern Ireland
//228 Switzerland (Confederation of)
//206 Belgium
//455 "Macao, China"
//525 Singapore (Republic of)
val restrictedCountryCodes =
setOf(
"460",
"461",
"454",
"466",
"440",
"441",
"450",
"234",
"235",
"228",
"206",
"455",
"525"
)
val currentCountryCode = getCountryCode(this)
return currentCountryCode !in restrictedCountryCodes
}
} }

View File

@ -136,7 +136,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME) binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME)
binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC) binding.descTv.text = intent.getStringExtra(PLAY_DETAILS_DESC)
val favoriteBeans = App.appFavoriteDBManager.getAllFavoriteBeans() val favoriteBeans = App.appFavoriteDBManager.getAllFavoriteBeans()
val allFilteredBeans = favoriteBeans.filter { it.isFavorite}//过滤只有为true的值 val allFilteredBeans = favoriteBeans.filter { it.isFavorite }//过滤只有为true的值
//找到当前点击进来的歌曲media //找到当前点击进来的歌曲media
val findCurrentMedia = allFilteredBeans.find { it.videoId == videoId }?.asMediaItem val findCurrentMedia = allFilteredBeans.find { it.videoId == videoId }?.asMediaItem
if (findCurrentMedia != null) { if (findCurrentMedia != null) {
@ -252,10 +252,6 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
if (currentScreenDownloads != null) { if (currentScreenDownloads != null) {
LogD(TAG, "updateDownloadUI id->${currentScreenDownloads.request.id}") LogD(TAG, "updateDownloadUI id->${currentScreenDownloads.request.id}")
updateDownloadUI(currentScreenDownloads) updateDownloadUI(currentScreenDownloads)
} else {
if (id != null) {
updateDownloadUi(id)
}
} }
} }
} }
@ -696,6 +692,12 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener {
updateDownloadUi(mediaItem.mediaId) updateDownloadUi(mediaItem.mediaId)
requests.trySend(Request.UpdateFavorite(mediaItem.mediaId))//更新喜欢状态 requests.trySend(Request.UpdateFavorite(mediaItem.mediaId))//更新喜欢状态
val currentDownload = DownloadUtil.getCurrentIdDownload(mediaItem.mediaId)
if (currentDownload != null) {
updateDownloadUI(currentDownload)
}
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(mediaItem.mediaMetadata.artworkUri) .load(mediaItem.mediaMetadata.artworkUri)

View File

@ -117,16 +117,20 @@ class MoSearchMoreActivity : MoBaseActivity() {
Innertube.moSearchPage(SearchBody(query = query, params = params))?.onSuccess { result -> Innertube.moSearchPage(SearchBody(query = query, params = params))?.onSuccess { result ->
if (result.isNotEmpty()) { if (result.isNotEmpty()) {
showDataUi() showDataUi()
val title = result[0].title var myResult = result[0]
if(myResult.searchResultList.isEmpty()){
myResult = result[1]
}
val title = myResult.title
if (title.isNullOrEmpty()) { if (title.isNullOrEmpty()) {
binding.title.visibility = View.GONE binding.title.visibility = View.GONE
} else { } else {
binding.title.visibility = View.VISIBLE binding.title.visibility = View.VISIBLE
binding.title.text = title binding.title.text = title
} }
currentContinuation = result[0].continuation currentContinuation = myResult.continuation
list.clear() list.clear()
list.addAll(result[0].searchResultList) list.addAll(myResult.searchResultList)
} else { } else {
showNoContentUi() showNoContentUi()
} }

View File

@ -10,6 +10,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.MediaPlayer import android.media.MediaPlayer
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.view.LayoutInflater import android.view.LayoutInflater
@ -55,15 +56,7 @@ class ImportFragment : Fragment() {
} }
binding.addBtn.setOnClickListener { binding.addBtn.setOnClickListener {
binding.addBtn.visibility = View.GONE binding.addBtn.visibility = View.GONE
if (ContextCompat.checkSelfPermission( checkAndRequestPermissions()
requireActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
openAudioPicker()
}
} }
importAdapterList.clear() importAdapterList.clear()
importAdapterList.addAll(relax.offline.music.App.importList) importAdapterList.addAll(relax.offline.music.App.importList)
@ -210,10 +203,9 @@ class ImportFragment : Fragment() {
binding.noContentLayout.visibility = View.GONE binding.noContentLayout.visibility = View.GONE
} else { } else {
binding.noContentLayout.visibility = View.VISIBLE binding.noContentLayout.visibility = View.VISIBLE
showNoDataDialog()
} }
binding.loadingLayout.visibility = View.GONE binding.loadingLayout.visibility = View.GONE
binding.addBtn.visibility = View.VISIBLE
} }
} }
} }
@ -222,4 +214,74 @@ class ImportFragment : Fragment() {
return musicFiles return musicFiles
} }
private fun showNoDataDialog() {
AlertDialog.Builder(requireActivity())
.setTitle(getString(R.string.prompt))
.setMessage(getString(R.string.no_data_prompt_dialog_content))
.setPositiveButton(getString(R.string.ok)) { dialog, _ ->
binding.addBtn.visibility = View.VISIBLE
dialog.dismiss()
}
.setCancelable(false)
.show()
}
private fun checkAndRequestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val permissionsToRequest = mutableListOf<String>()
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_IMAGES
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_IMAGES)
}
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_VIDEO
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_VIDEO)
}
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_MEDIA_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_AUDIO)
}
if (permissionsToRequest.isNotEmpty()) {
requestMultiplePermissionsLauncher.launch(permissionsToRequest.toTypedArray())
} else {
openAudioPicker()
}
} else {
// 请求旧版本的权限
if (ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} else {
openAudioPicker()
}
}
}
private val requestMultiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.forEach { (permission, isGranted) ->
if (isGranted) {
openAudioPicker()
} else {
showExplanationDialog()
}
}
}
} }

View File

@ -0,0 +1,186 @@
package relax.offline.music.http
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.media.MediaDrm
import android.os.Build
import android.telephony.TelephonyManager
import android.text.TextUtils
import relax.offline.music.App
import relax.offline.music.sp.AppStore
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.UUID
val packageName: String = App.app.packageName
fun getCountryCode(context: Context): String? {
var countryCode = ""
try {
val telephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
countryCode = if (telephonyManager.phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
val c = Class.forName("android.os.SystemProperties")
val get = c.getMethod("get", String::class.java)
// Gives MCC + MNC
val homeOperator = get.invoke(c, "ro.cdma.home.operator.numeric") as String
homeOperator.substring(0, 3) // the last three digits is MNC
} else {
val config = context.resources.configuration
config.mcc.toString()
}
} catch (e: Exception) {
e.printStackTrace()
}
return countryCode
}
fun getSimCountryIso(context: Context): String? {
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.simCountryIso
}
//获取国家代码
fun getCountry(context: Context): String? {
return getSimCountryIso(context)
}
fun getAppVersionName(context: Context): String? {
val packageManager = context.packageManager
var packInfo: PackageInfo? = null
try {
packInfo = packageManager.getPackageInfo(context.packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return packInfo!!.versionName
}
fun getAppVersionCode(context: Context): Long {
return try {
val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.getPackageInfo(context.packageName, PackageManager.PackageInfoFlags.of(0))
} else {
context.packageManager.getPackageInfo(context.packageName, 0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
-1
}
}
fun userID(context: Context): String {
var id = ""
if (TextUtils.isEmpty(AppStore(context).userID)) {
val newID = UUID.randomUUID().toString()
AppStore(context).userID = newID
} else {
id = AppStore(context).userID
}
return id
}
fun getUUId(ctx: Context): String {
val cache = AppStore(ctx).appUUID
return if (TextUtils.isEmpty(cache)) {
val u = generateImmutableUUID(ctx)
AppStore(ctx).appUUID = u
u
} else {
cache
}
}
fun generateImmutableUUID(context: Context): String {
/*The Android ID
* 通常被认为不可信因为它有时为null开发文档中说明了这个ID会改变如果进行了出厂设置
* 并且如果某个Andorid手机被Root过的话这个ID也可以被任意改变
* */
val m_szAndroidID = android.provider.Settings.Secure.getString(
context.contentResolver,
android.provider.Settings.Secure.ANDROID_ID
)
/*
*Android系统2.3版本以上可以获取硬件Serial Number
* 优点非手机设备也可以通过该接口获取ID
* */
var serial: String? = null
serial = try {
//API>=9 使用serial号
android.os.Build::class.java.getField("SERIAL").get(null).toString()
} catch (exception: Exception) {
"serial" // serial需要一个初始化随便一个初始化
}
//drm id
var drmId: String = getDrmDeviceId()
/*
* 有一些特殊的情况一些如平板电脑的设置没有通话功能或者你不愿加入READ_PHONE_STATE许可
* 而你仍然想获得唯一序列号之类的东西这时你可以通过取出ROM版本制造商CPU型号以及其他硬件信息来实现这一点
* 这样计算出来的ID不是唯一的因为如果两个手机应用了同样的硬件以及Rom 镜像
* 但应当明白的是出现类似情况的可能性基本可以忽略要实现这一点你可以使用Build类:
* */
val m_szDevIDShort = "666" +
Build.HOST.length % 10 +
Build.ID.length % 10 +
Build.MANUFACTURER.length % 10 +
Build.MODEL.length % 10 +
Build.PRODUCT.length % 10 +
Build.TAGS.length % 10 +
Build.TYPE.length % 10 +
Build.BOARD.length % 10 +
Build.BRAND.length % 10 +
Build.CPU_ABI.length % 10 +
Build.DEVICE.length % 10 +
Build.DISPLAY.length % 10 +
Build.USER.length % 10 //13 digits
val m_szLongID = serial + m_szDevIDShort + m_szAndroidID + drmId
//md5加密生成唯一uuid
var m: MessageDigest? = null
try {
m = MessageDigest.getInstance("MD5")
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
m!!.update(m_szLongID.toByteArray(), 0, m_szLongID.length)
// get md5 bytes
val p_md5Data = m!!.digest()
// create a hex string
var m_szUniqueID = StringBuilder()
for (aP_md5Data in p_md5Data) {
val b = 0xFF and aP_md5Data.toInt()
// if it is a single digit, make sure it have 0 in front (proper padding)
if (b <= 0xF)
m_szUniqueID.append("0")
// add number to string
m_szUniqueID.append(Integer.toHexString(b))
} // hex string to uppercase
m_szUniqueID = StringBuilder(m_szUniqueID.toString().toLowerCase())
return m_szUniqueID.toString()
}
private fun getDrmDeviceId(): String {
var id = ""
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val COMMON_PSSH_UUID = UUID(0x1077EFECC0B24D02L, -0x531cc3e1ad1d04b5L)
val CLEARKEY_UUID = UUID(-0x1d8e62a7567a4c37L, 0x781AB030AF78D30EL)
val PLAYREADY_UUID = UUID(-0x65fb0f8667bfbd7aL, -0x546d19a41f77a06bL)
try {
val bytesId = MediaDrm(WIDEVINE_UUID)
.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID)
id = android.util.Base64.encodeToString(bytesId, android.util.Base64.NO_WRAP)
} catch (ignore: Exception) {
}
return id
}

View File

@ -0,0 +1,103 @@
package relax.offline.music.http
import android.content.Context
import com.google.gson.Gson
import org.json.JSONException
import org.json.JSONObject
import relax.offline.music.App
import relax.offline.music.util.AesEncryptUtil
import java.util.Locale
import java.util.UUID
class BaseApiUtil {
companion object {
val shared: BaseApiUtil by lazy { BaseApiUtil() }
const val initialAddress = "/"
const val key = "7387m0ax5x0n5tca"
const val iv = "D4Q1ymaIHSYifWS9"
const val getIPInfoUrl = "/app/common/getIPInfo"
const val getAppDataURL = "/statistic/appdatacollection/saveAppData"
const val BaseUrl = "https://api.tikustok.com"
}
private fun PostContentMessage.toJson(): String {
val gson = Gson()
return gson.toJson(this)
}
private fun PostContentSecurityBody.toJson(): String {
val gson = Gson()
return gson.toJson(this)
}
fun initPostBody(
method: String,
url: String,
query: HashMap<String, String>,
json: JSONObject? = null,
currentBaseUrl: String? = null
): String {
val randomStr = UUID.randomUUID().toString().replace("-", "")
val headers: HashMap<String, String> = HashMap()
headers["request_userid"] = userID(App.app)
headers["request_pkgname"] = packageName
headers["base_url"] = currentBaseUrl + ""
headers["request_deviceid"] = getUUId(App.app)
headers["request_id"] = UUID.randomUUID().toString()
val requestId = UUID.randomUUID().toString()
val timestamp = "TS" + System.currentTimeMillis()
val trueBody = PostContentMessage(
requestId,
method,
url,
query,
headers,
json.toString(),
timestamp,
randomStr
)
val jsonString = trueBody.toJson()
val encryptJsonString = AesEncryptUtil.encrypt(jsonString, key, iv)
val postContentSecurityBody = PostContentSecurityBody(method, encryptJsonString)
return postContentSecurityBody.toJson()
}
fun postJson(context: Context, eventName: String?): JSONObject {
val jsonObject = JSONObject()
try {
jsonObject.put("eventName", eventName)
jsonObject.put("timestamp", System.currentTimeMillis())
jsonObject.put("uuid", getUUId(context))
jsonObject.put("app_version_name", getAppVersionName(context))
jsonObject.put("app_version_code", getAppVersionCode(context))
jsonObject.put("channel", "google")
jsonObject.put("country", getCountry(context))
jsonObject.put("device", "android")
jsonObject.put("language", Locale.getDefault().language)
jsonObject.put("pkgName", context.packageName)
jsonObject.put("userId", userID(context))
} catch (e: Exception) {
e.printStackTrace()
}
return jsonObject
}
//需要添加gaid的json组装
fun postJson(context: Context, eventName: String?, gaId: String?): JSONObject {
val jsonObject = postJson(context, eventName)
try {
jsonObject.put("adId", gaId)
} catch (e: JSONException) {
throw RuntimeException(e)
}
return jsonObject
}
}

View File

@ -0,0 +1,57 @@
package relax.offline.music.http
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.json.JSONObject
import relax.offline.music.App
import relax.offline.music.sp.AppStore
import relax.offline.music.util.AesEncryptUtil
import relax.offline.music.util.LogTag
import java.io.IOException
class CommonIpInfoUtil {
private val TAG = LogTag.VO_API_LOG
companion object {
val shared: CommonIpInfoUtil by lazy { CommonIpInfoUtil() }
}
fun initIPInfo() {
try {
MyHttpUtil.mInstance.getIPInfo(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
try {
LogTag.LogD(TAG, "getIPInfo response->${response}")
if (response.code == 200) {
val responseData = response.body?.string()
if (responseData != null) {
val jsonObject = JSONObject(responseData)
val status = jsonObject.optString("status")
if (status == "Success") {
val data = jsonObject.optString("data")
val decryptData = AesEncryptUtil.desEncrypt(data)
val jsonObjectDecryptData = JSONObject(decryptData)
LogTag.LogD(TAG, "getIPInfo data->${jsonObjectDecryptData}")
val isoCode = jsonObjectDecryptData.optString("isoCode")
AppStore(App.app).ipCountryCode = isoCode
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@ -0,0 +1,10 @@
package relax.offline.music.http
import java.io.IOException
interface MyCallback {
fun onFailure(message: String?)
@Throws(IOException::class)
fun onSuccess(data: String)
}

View File

@ -0,0 +1,90 @@
package relax.offline.music.http
import android.content.Context
import android.net.ConnectivityManager
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.TimeUnit
class MyHttpUtil {
fun isNetworkAvailable(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetworkInfo = connectivityManager.activeNetworkInfo
return activeNetworkInfo != null && activeNetworkInfo.isConnected
}
private var mOkHttpClient: OkHttpClient? = null
companion object {
val mInstance: MyHttpUtil by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
MyHttpUtil()
}
}
private val CONNECT_TIMEOUT: Long = 60 //超时时间,秒
private val READ_TIMEOUT: Long = 60 //读取时间,秒
private val WRITE_TIMEOUT: Long = 60 //写入时间,秒
init {
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
.connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
mOkHttpClient = builder.build()
}
private val mediaType = "application/json; charset=utf-8".toMediaType()
@Throws(IOException::class)
fun postSaveAppData(json: JSONObject, callback: Callback) {
val query: HashMap<String, String> = HashMap()
val body = BaseApiUtil.shared.initPostBody(
"POST",
BaseApiUtil.getAppDataURL,
query,
json
)
val path = BaseApiUtil.BaseUrl + BaseApiUtil.initialAddress
post(path, body, callback)
}
fun getIPInfo(callback: Callback) {
val query: HashMap<String, String> = HashMap()
val body = BaseApiUtil.shared.initPostBody(
"GET",
BaseApiUtil.getIPInfoUrl,
query
)
val path = BaseApiUtil.BaseUrl+ BaseApiUtil.initialAddress
post(path, body, callback)
}
fun post(path: String, postBody: String, callback: Callback) {
val requestBody = postBody.toRequestBody(mediaType)
val request: Request = Request.Builder()
.url(path)
.post(requestBody)
.build()
doAsync(request, callback)
}
/**
* 异步请求
*/
@Throws(IOException::class)
private fun doAsync(request: Request, callback: Callback) {
//创建请求会话
val call = mOkHttpClient!!.newCall(request)
//同步执行会话请求
call.enqueue(callback)
}
}

View File

@ -0,0 +1,14 @@
package relax.offline.music.http
import java.io.Serializable
data class PostContentMessage(
var requestId: String,//uuid
var method: String,// 请求原接口的http方法 GETPOST
var url: String, // 原接口url
var query: HashMap<String, String>,//原接口传的url参数
var headers: HashMap<String, String>,// 原接口传的head
var body: String,// 原接口post数据的json
var timestamp: String,// 时间戳
var randomStr: String//随机字符串和url上的一致
) : Serializable

View File

@ -0,0 +1,8 @@
package relax.offline.music.http
import java.io.Serializable
data class PostContentSecurityBody(
var method: String,
var body: String,
): Serializable

View File

@ -0,0 +1,123 @@
package relax.offline.music.http
import android.content.Context
import okhttp3.Call
import okhttp3.Response
import org.json.JSONObject
import relax.offline.music.sp.AppStore
import relax.offline.music.util.LogTag
import java.io.IOException
class UploadEventName {
private val TAG = LogTag.VO_API_LOG
companion object {
val shared: UploadEventName by lazy { UploadEventName() }
}
fun init(context: Context) {
if (MyHttpUtil.mInstance.isNetworkAvailable(context)) {
initFirstOpen(context)
initStartProgram(context)
}
}
private fun initFirstOpen(context: Context) {
if (!AppStore(context).firstOpenIsSucceed) {
try {
MyHttpUtil.mInstance.postSaveAppData(
BaseApiUtil.shared.postJson(context, "first_open"),
object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
try {
LogTag.LogD(TAG, "first_open response->${response}")
if (response.code == 200) {
val responseData = response.body?.string()
if (responseData != null) {
val jsonObject = JSONObject(responseData)
val status = jsonObject.getString("status")
LogTag.LogD(TAG, "first_open jsonObject->${jsonObject}")
LogTag.LogD(TAG, "first_open status->${status}")
if (status.equals("Success")) {
AppStore(context).firstOpenIsSucceed = true
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
})
} catch (e: Exception) {
LogTag.LogD(TAG, e.toString())
}
}
}
private fun initStartProgram(context: Context) {
try {
MyHttpUtil.mInstance.postSaveAppData(
BaseApiUtil.shared.postJson(context, "app_open"),
object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
}
override fun onResponse(call: Call, response: Response) {
try {
LogTag.LogD(TAG, "start_program response->${response}")
if (response.code == 200) {
val responseData = response.body?.string()
if (responseData != null) {
val jsonObject = JSONObject(responseData)
val status = jsonObject.getString("status")
LogTag.LogD(TAG, "start_program jsonObject->${jsonObject}")
LogTag.LogD(TAG, "start_program status->${status}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
})
} catch (e: Exception) {
LogTag.LogE(TAG, e.toString())
}
}
fun initAppEventData(json: JSONObject) {
try {
MyHttpUtil.mInstance.postSaveAppData(
json,
object : okhttp3.Callback {
override fun onFailure(call: Call, e: IOException) {
LogTag.LogD(TAG, "AppEventData onFailure->${e}")
}
override fun onResponse(call: Call, response: Response) {
try {
LogTag.LogD(TAG, "AppEventData response->${response}")
if (response.code == 200) {
val responseData = response.body?.string()
if (responseData != null) {
val jsonObject = JSONObject(responseData)
val status = jsonObject.getString("status")
LogTag.LogD(TAG, "AppEventData jsonObject->${jsonObject}")
LogTag.LogD(TAG, "AppEventData status->${status}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
})
} catch (e: Exception) {
LogTag.LogE(TAG, e.toString())
}
}
}

View File

@ -98,7 +98,7 @@ class PlaybackService : MediaSessionService(), Player.Listener {
// setSmallIcon(R.mipmap.app_logo_img) // setSmallIcon(R.mipmap.app_logo_img)
// } // }
setMediaNotificationProvider(DefaultMediaNotificationProvider(this).apply { setMediaNotificationProvider(DefaultMediaNotificationProvider(this).apply {
setSmallIcon(R.mipmap.app_logo) setSmallIcon(R.mipmap.app_logo_no_bg)
}) })
} }

View File

@ -27,10 +27,33 @@ class AppStore(context: Context) {
defaultValue = PlayMode.LIST_LOOP.value defaultValue = PlayMode.LIST_LOOP.value
) )
var userID: String by store.string(
key = APP_USERID,
defaultValue = ""
)
var appUUID : String by store.string(
key = APP_UUID,
defaultValue = ""
)
var ipCountryCode: String by store.string(
key = IP_COUNTRY_CODE,
defaultValue = ""
)
var firstOpenIsSucceed : Boolean by store.boolean(
key = FIRST_OPEN_IS_SUCCEED,
defaultValue = false
)
companion object { companion object {
private const val FILE_NAME = "music_oo_app" private const val FILE_NAME = "music_oo_app"
const val SEARCH_HISTORY = "search_history" const val SEARCH_HISTORY = "search_history"
const val MY_VISITOR_DATA = "my_visitor_data" const val MY_VISITOR_DATA = "my_visitor_data"
const val PLAY_MUSIC_MODE = "play_music_mode" const val PLAY_MUSIC_MODE = "play_music_mode"
const val APP_USERID = "app_userid"
const val APP_UUID = "app_uuid"
const val IP_COUNTRY_CODE = "ip_country_code"
const val FIRST_OPEN_IS_SUCCEED = "first_open_is_succeed"
} }
} }

View File

@ -0,0 +1,64 @@
package relax.offline.music.util;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import relax.offline.music.http.BaseApiUtil;
public class AesEncryptUtil {
private static final String AES_MODE = "AES/CBC/NOPadding";
public static String encrypt(String data, String key, String iv) {
try {
Cipher cipher = Cipher.getInstance(AES_MODE);
int blockSize = cipher.getBlockSize();
byte[] dataBytes = data.getBytes();
int plaintextLength = dataBytes.length;
if (plaintextLength % blockSize != 0) {
plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
}
byte[] plaintext = new byte[plaintextLength];
System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
byte[] encrypted = cipher.doFinal(plaintext);
return Base64.encodeBytes(encrypted);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String desEncrypt(String data) {
return desEncrypt(data, BaseApiUtil.key, BaseApiUtil.iv);
}
/**
* 解密方法
*
* @param data 要解密的数据
* @param key 解密key
* @param iv 解密iv
* @return 解密的结果
* @throws Exception
*/
public static String desEncrypt(String data, String key, String iv) {
try {
byte[] encrypted1 = Base64.decode(data);
Cipher cipher = Cipher.getInstance(AES_MODE);
SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
byte[] original = cipher.doFinal(encrypted1);
String originalString = new String(original);
return originalString.trim();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -171,7 +171,7 @@ object DownloadUtil {
return isExist return isExist
} }
fun getCurrentIdDownloadState(id: String): Int { fun getCurrentIdDownload(id: String): Download? {
if (downloadManager != null) { if (downloadManager != null) {
val downloadIndex = downloadManager!!.downloadIndex val downloadIndex = downloadManager!!.downloadIndex
downloadIndex.getDownloads() downloadIndex.getDownloads()
@ -179,12 +179,12 @@ object DownloadUtil {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val download = cursor.download val download = cursor.download
if (download.request.id == id) { if (download.request.id == id) {
return download.state return download
} }
} }
} }
} }
return -1 return null
} }
fun getDownloadCount(): Int { fun getDownloadCount(): Int {

View File

@ -1,6 +1,7 @@
package relax.offline.music.util package relax.offline.music.util
import android.util.Log import android.util.Log
import relax.offline.music.BuildConfig
object LogTag { object LogTag {
const val VO_ACT_LOG = "vo-act—log" const val VO_ACT_LOG = "vo-act—log"
@ -11,14 +12,14 @@ object LogTag {
const val VO_TEST_ONLY = "vo-only—log" const val VO_TEST_ONLY = "vo-only—log"
fun LogD(tag: String, message: String) { fun LogD(tag: String, message: String) {
Log.d(tag, message) if (BuildConfig.DEBUG) {
Log.d(tag, message)
}
} }
fun LogE(tag: String, message: String) { fun LogE(tag: String, message: String) {
Log.e(tag, message) if (BuildConfig.DEBUG) {
} Log.e(tag, message)
}
fun LogI(tag: String, message: String) {
Log.i(tag, message)
} }
} }

View File

@ -57,6 +57,16 @@ class SearchResultOptimalView(context: Context, data: Innertube.SearchDataPage)
) )
context.startActivity(intent) context.startActivity(intent)
} }
"MUSIC_PAGE_TYPE_PLAYLIST" -> {
val intent = Intent(context, MoListDetailsActivity::class.java)
intent.putExtra(
MoListDetailsActivity.PLAY_LIST_PAGE_BROWSE_ID,
optimalBean.browseId
)
context.startActivity(intent)
}
else -> { else -> {
val intent = Intent(context, MoSingerDetailsActivity::class.java) val intent = Intent(context, MoSingerDetailsActivity::class.java)
intent.putExtra( intent.putExtra(

View File

@ -78,6 +78,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical"
android:visibility="visible"> android:visibility="visible">
<ImageView <ImageView

View File

@ -190,6 +190,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="18dp" android:layout_marginStart="18dp"
android:layout_marginEnd="18dp" android:layout_marginEnd="18dp"
android:visibility="gone"
android:fontFamily="@font/medium_font" android:fontFamily="@font/medium_font"
android:text="@string/new_playlist" android:text="@string/new_playlist"
android:textColor="@color/white" android:textColor="@color/white"

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -36,4 +36,6 @@
<string name="new_playlist">New playlist</string> <string name="new_playlist">New playlist</string>
<string name="liked_songs_no_data_prompt">You haven\'t liked any songs yet.</string> <string name="liked_songs_no_data_prompt">You haven\'t liked any songs yet.</string>
<string name="offline_songs_no_data_prompt">You haven\'t saved any songs for offline listening yet.</string> <string name="offline_songs_no_data_prompt">You haven\'t saved any songs for offline listening yet.</string>
<string name="no_data_prompt_dialog_content">It looks like there\'s no data. Please add some and try again.</string>
<string name="prompt">Prompt</string>
</resources> </resources>