diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f835f67a8..e4472b95d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,6 +5,7 @@ plugins { alias(libs.plugins.application) alias(libs.plugins.kotlin) alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.kotlin.serialization) alias(libs.plugins.ksp) alias(libs.plugins.room) } @@ -214,6 +215,7 @@ androidComponents { dependencies { implementation(libs.kotlinx.coroutines) + implementation(libs.kotlinx.serialization.json) implementation(libs.appcompat) implementation(libs.cardview) @@ -248,7 +250,6 @@ dependencies { implementation(libs.rebound) implementation(libs.rxandroid) implementation(libs.rxjava) - implementation(libs.xxpermissions) implementation(libs.sardine.android) { // https://github.com/thegrizzlylabs/sardine-android/issues/70 // 上游已经exclude了,但是不知道为什么还是会有 @@ -258,6 +259,7 @@ dependencies { implementation(libs.slf4j) implementation(libs.timber) implementation(libs.tinypinyin) + implementation(libs.xxpermissions) debugImplementation(libs.leakcanary) diff --git a/app/src/main/java/remix/myplayer/App.kt b/app/src/main/java/remix/myplayer/App.kt index 65b2bfc0a..4ca43ab35 100644 --- a/app/src/main/java/remix/myplayer/App.kt +++ b/app/src/main/java/remix/myplayer/App.kt @@ -6,7 +6,6 @@ import android.os.Build import android.os.Process import androidx.multidex.MultiDex import androidx.multidex.MultiDexApplication -import com.hjq.permissions.XXPermissions import com.tencent.bugly.crashreport.CrashReport import com.tencent.bugly.crashreport.CrashReport.UserStrategy import io.reactivex.Completable @@ -60,17 +59,6 @@ class App : MultiDexApplication() { } private fun checkMigration() { - if (!SPUtil.getValue(context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.LYRIC_RESET_ON_16000, false)) { - SPUtil.deleteFile(this, SPUtil.LYRIC_KEY.NAME) - SPUtil.putValue(context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.LYRIC_RESET_ON_16000, true) - SPUtil.putValue(context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.PRIORITY_LYRIC, SPUtil.LYRIC_KEY.DEFAULT_PRIORITY) -// try { -// DiskCache.getLrcDiskCache().delete() -// } catch (e: Exception) { -// Timber.v(e) -// } - } - val oldVersion = SPUtil.getValue(context, SETTING_KEY.NAME, SETTING_KEY.VERSION, 1) if (oldVersion < SETTING_KEY.NEWEST_VERSION) { if (oldVersion == 1) { @@ -85,8 +73,6 @@ class App : MultiDexApplication() { } private fun setUp() { - XXPermissions.setCheckMode(false) - DiskCache.init(this, "lyric") setApplicationLanguage(this) Completable .fromAction { diff --git a/app/src/main/java/remix/myplayer/bean/misc/LyricPriority.kt b/app/src/main/java/remix/myplayer/bean/misc/LyricPriority.kt deleted file mode 100644 index 3097b653f..000000000 --- a/app/src/main/java/remix/myplayer/bean/misc/LyricPriority.kt +++ /dev/null @@ -1,48 +0,0 @@ -package remix.myplayer.bean.misc - -import remix.myplayer.App -import remix.myplayer.R -import java.util.* - -enum class LyricPriority(val priority: Int, val desc: String) { - DEF(0, App.context.getString(R.string.default_lyric_priority)), - IGNORE(1, App.context.getString(R.string.ignore_lrc)), - EMBEDDED(2, App.context.getString(R.string.embedded_lyric)), - LOCAL(3, App.context.getString(R.string.local)), - KUGOU(4, App.context.getString(R.string.kugou)), - NETEASE(5, App.context.getString(R.string.netease)), - QQ(6, App.context.getString(R.string.qq)), - MANUAL(7, App.context.getString(R.string.select_lrc)); - - override fun toString(): String { - return desc - } - - companion object { - @JvmStatic - fun toLyricPrioritys(list: List): List { - val prioritys = ArrayList() - list.forEach { - prioritys.add(toLyricPriority(it)) - } - return prioritys - } - - @JvmStatic - fun toLyricPriority(desc: CharSequence): LyricPriority { - return when (desc) { - DEF.desc -> DEF - IGNORE.desc -> IGNORE - NETEASE.desc -> NETEASE - KUGOU.desc -> KUGOU - QQ.desc -> QQ - LOCAL.desc -> LOCAL - EMBEDDED.desc -> EMBEDDED - MANUAL.desc -> MANUAL - else -> DEF - } - } - } - - -} diff --git a/app/src/main/java/remix/myplayer/helper/LyricsHelper.kt b/app/src/main/java/remix/myplayer/helper/LyricsHelper.kt new file mode 100644 index 000000000..228fcf924 --- /dev/null +++ b/app/src/main/java/remix/myplayer/helper/LyricsHelper.kt @@ -0,0 +1,34 @@ +package remix.myplayer.helper + +import android.content.Context +import remix.myplayer.R +import remix.myplayer.theme.Theme +import remix.myplayer.util.SPUtil + +object LyricsHelper { + fun showLocalLyricsTip(context: Context, action: () -> Unit) { + if (!SPUtil.getValue( + context, + SPUtil.LYRICS_KEY.NAME, + SPUtil.LYRICS_KEY.LOCAL_LYRICS_TIP_SHOWN, + false + ) + ) { + Theme.getBaseDialog(context) + .positiveText(R.string.confirm) + .onPositive { _, _ -> + SPUtil.putValue( + context, + SPUtil.LYRICS_KEY.NAME, + SPUtil.LYRICS_KEY.LOCAL_LYRICS_TIP_SHOWN, + true + ) + action.invoke() + } + .content(R.string.local_lyrics_tip) + .show() + } else { + action.invoke() + } + } +} diff --git a/app/src/main/java/remix/myplayer/helper/MusicServiceRemote.kt b/app/src/main/java/remix/myplayer/helper/MusicServiceRemote.kt index d233e232b..d10c43165 100644 --- a/app/src/main/java/remix/myplayer/helper/MusicServiceRemote.kt +++ b/app/src/main/java/remix/myplayer/helper/MusicServiceRemote.kt @@ -135,7 +135,7 @@ object MusicServiceRemote { } @JvmStatic - fun setProgress(progress: Int) { + fun setProgress(progress: Long) { service?.setProgress(progress) } @@ -163,9 +163,4 @@ object MusicServiceRemote { fun getOperation(): Int { return service?.operation ?: Command.NEXT } - - @JvmStatic - fun setLyricOffset(offset: Int) { - service?.setLyricOffset(offset) - } } diff --git a/app/src/main/java/remix/myplayer/lyric/DefaultLrcParser.kt b/app/src/main/java/remix/myplayer/lyric/DefaultLrcParser.kt deleted file mode 100644 index b8c50e5a9..000000000 --- a/app/src/main/java/remix/myplayer/lyric/DefaultLrcParser.kt +++ /dev/null @@ -1,149 +0,0 @@ -package remix.myplayer.lyric - -import android.os.Environment -import android.text.TextUtils -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import remix.myplayer.App -import remix.myplayer.lyric.bean.LrcRow -import remix.myplayer.misc.cache.DiskCache -import java.io.BufferedReader -import java.io.File - -/** - * @ClassName - * @Description 解析歌词实现类 - * @Author Xiaoborui - * @Date 2016/10/28 09:50 - */ - -class DefaultLrcParser : ILrcParser { - override fun saveLrcRows(lrcRows: List?, cacheKey: String?, searchKey: String?) { - if (lrcRows == null || lrcRows.isEmpty()) - return - - //缓存 - if (!cacheKey.isNullOrEmpty()) { - DiskCache.getLrcDiskCache()?.apply { - edit(cacheKey)?.apply { - newOutputStream(0)?.use { outStream -> - outStream.write(Gson().toJson(lrcRows, object : TypeToken>() {}.type).toByteArray()) - } - }?.commit() - }?.flush() - } - - //保存歌词原始文件 - if (TextUtils.isEmpty(searchKey) || Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()) - return - - if (!searchKey.isNullOrEmpty()) { - File(Environment.getExternalStorageDirectory(), "Android/data/" - + App.context.packageName + "/lyric") - .run { - //目录 - if (exists() || mkdirs()) - File(this, searchKey.replace("/".toRegex(), "") + ".lrc") - else null - }?.run { - //文件不存在或者成功重新创建 - if (!exists() || (delete() && createNewFile())) - this - else null - }?.run { - lrcRows.forEach { lrcRow -> - val strBuilder = StringBuilder(128) - strBuilder.append("[") - strBuilder.append(lrcRow.timeStr) - strBuilder.append("]") - strBuilder.append(lrcRow.content).append("\n") - if (lrcRow.hasTranslate()) { - strBuilder.append("[") - strBuilder.append(lrcRow.timeStr) - strBuilder.append("]") - strBuilder.append(lrcRow.translate).append("\n") - } - appendText(strBuilder.toString()) - } - } - } - } - - override fun getLrcRows(bufferedReader: BufferedReader?, needCache: Boolean, cacheKey: String?, searchKey: String?): List { - - //解析歌词 - val lrcRows = ArrayList() - val allLine = ArrayList() - var offset = 0 - if (bufferedReader == null) - return lrcRows - bufferedReader.useLines { allLines -> - allLines.forEach { eachLine -> - allLine.add(eachLine) - //读取offset标签 - if (eachLine.startsWith("[offset:") && eachLine.endsWith("]")) { - val offsetInString = eachLine.substring(eachLine.lastIndexOf(":") + 1, eachLine.length - 1) - if (offsetInString.isNotEmpty() && TextUtils.isDigitsOnly(offsetInString)) { - offset = Integer.valueOf(offsetInString) - } - } - } - } - - if (allLine.size == 0) - return lrcRows - - for (temp in allLine) { - //解析每一行歌词 - val rows = LrcRow.createRows(temp, offset) - if (rows != null && rows.size > 0) - lrcRows.addAll(rows) - } - - lrcRows.sort() - //合并翻译 - val combineLrcRows = ArrayList() - var index = 0 - while (index < lrcRows.size) { - // 判断下一句歌词和当前歌词的时间是否一致,一致则认为下一句是当前歌词的翻译 - val currentRow = lrcRows[index] - val nextRow = lrcRows.getOrNull(index + 1) - if (currentRow.time == nextRow?.time && !currentRow.content.isNullOrBlank()) { // 带翻译的歌词 - val tmp = LrcRow() - tmp.content = currentRow.content - tmp.time = currentRow.time - tmp.timeStr = currentRow.timeStr - tmp.translate = nextRow.content - combineLrcRows.add(tmp) - index++ - } else { // 普通歌词 - combineLrcRows.add(currentRow) - } - index++ - } - - lrcRows.clear() - lrcRows.addAll(combineLrcRows) - - - if (lrcRows.size == 0) - return lrcRows - lrcRows.sort() - //每行歌词的时间 - for (i in 0 until lrcRows.size - 1) { - lrcRows[i].setTotalTime(lrcRows[i + 1].time - lrcRows[i].time) - } - lrcRows[lrcRows.size - 1].setTotalTime(5000) - //缓存 - if (needCache) { - saveLrcRows(lrcRows, cacheKey, searchKey) - } - - return lrcRows - } - - companion object { - const val TAG = "DefaultLrcParser" - const val THRESHOLD_PROPORTION = 0.3 - } -} diff --git a/app/src/main/java/remix/myplayer/lyric/ILrcParser.kt b/app/src/main/java/remix/myplayer/lyric/ILrcParser.kt deleted file mode 100644 index e6c0cfe9b..000000000 --- a/app/src/main/java/remix/myplayer/lyric/ILrcParser.kt +++ /dev/null @@ -1,16 +0,0 @@ -package remix.myplayer.lyric - -import remix.myplayer.lyric.bean.LrcRow -import java.io.BufferedReader - -/** - * @ClassName - * @Description - * @Author Xiaoborui - * @Date 2016/10/28 09:48 - */ -interface ILrcParser { - fun saveLrcRows(lrcRows: List?, cacheKey: String?, searchKey: String?) - fun getLrcRows(bufferedReader: BufferedReader?, needCache: Boolean, cacheKey: String?, - searchKey: String?): List -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyric/ILrcView.kt b/app/src/main/java/remix/myplayer/lyric/ILrcView.kt deleted file mode 100644 index efaa0c1d4..000000000 --- a/app/src/main/java/remix/myplayer/lyric/ILrcView.kt +++ /dev/null @@ -1,35 +0,0 @@ -package remix.myplayer.lyric - -import remix.myplayer.lyric.bean.LrcRow - -interface ILrcView { - /** - * 初始化画笔,颜色,字体大小等设置 - */ - fun init() - - /*** - * 设置数据源 - * @param lrcRows - */ - fun setLrcRows(lrcRows: List?) - - /** - * 指定时间 - * - * @param progress 时间进度 - * @param fromSeekBarByUser 是否由用户触摸Seekbar触发 - */ - fun seekTo(progress: Int, fromSeekBar: Boolean, fromSeekBarByUser: Boolean) - - /*** - * 设置歌词文字的缩放比例 - * @param newFactor - */ - fun setLrcScalingFactor(newFactor: Float) - - /** - * 重置 - */ - fun reset() -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyric/LrcView.kt b/app/src/main/java/remix/myplayer/lyric/LrcView.kt deleted file mode 100644 index de6af4e60..000000000 --- a/app/src/main/java/remix/myplayer/lyric/LrcView.kt +++ /dev/null @@ -1,767 +0,0 @@ -package remix.myplayer.lyric - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Rect -import android.os.Handler -import android.os.Looper -import android.text.Layout -import android.text.StaticLayout -import android.text.TextPaint -import android.util.AttributeSet -import android.util.TypedValue -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import android.view.animation.DecelerateInterpolator -import android.view.animation.Interpolator -import android.widget.Scroller -import androidx.annotation.ColorInt -import androidx.annotation.StringRes -import remix.myplayer.App -import remix.myplayer.R -import remix.myplayer.lyric.bean.LrcRow -import remix.myplayer.theme.Theme -import remix.myplayer.util.DensityUtil -import remix.myplayer.util.SPUtil -import timber.log.Timber -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -/** - * Created by Remix on 2018/1/3. - */ -class LrcView : View, ILrcView { - /** - * 所有的歌词 - */ - private var lrcRows: List? = null - - /** - * 所有歌词总计高度 - */ - private var totalHeight = 0 - - /** - * 画高亮歌词的画笔 - */ - private val highLightPaint by lazy { - TextPaint() - } - - /** - * 高亮歌词当前的字体颜色 - */ - private var highLightTextColor = DEFAULT_COLOR_FOR_HIGH_LIGHT_LRC - - /** - * 画其他歌词的画笔 - */ - private val normalPaint by lazy { - TextPaint() - } - /** - * 高亮歌词当前的字体颜色 - */ - private var normalTextColor = DEFAULT_COLOR_FOR_OTHER_LRC - - /** - * 画时间线的画笔 - */ - private val timeLinePaint: TextPaint by lazy { - TextPaint() - } - - /***时间线的颜色 */ - private var timeLineTextColor = Color.GRAY - - /** - * 时间文字大小 - */ - private var timeLineTextSize = 0f - - /** - * 是否画时间线 - */ - private var isDrawTimeLine = false - - /** - * 每一句歌词之间的行距 - */ - private var linePadding = DEFAULT_PADDING - /** - * 返回当前的歌词缩放比例 - */ - /** - * 歌词的当前缩放比例 - */ - var scalingFactor = - SPUtil.getValue(App.context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.LYRIC_FONT_SIZE, "1f") - .toFloat() - private set - - /** - * 实现歌词竖直方向平滑滚动的辅助对象 - */ - private val scroller: Scroller by lazy { - Scroller(context, DEFAULT_INTERPOLATOR) - } - - /** - * 插值器 - */ - private val DEFAULT_INTERPOLATOR: Interpolator = DecelerateInterpolator() - - /** - * 控制文字缩放的因子 - */ - private var curFraction = 0f - private var touchSlop = 0 - - /** - * 错误提示文字 - */ - private var placeholder = App.context.getString(R.string.no_lrc) - - /** - * 当前纵坐标 - */ - private var rowY = 0f - - /** - * 时间线的图标 - */ - private val timelineDrawable = Theme - .getDrawable(App.context, R.drawable.icon_lyric_timeline) - - /** - * 初始状态时间线图标所在的位置 - */ - private val timelineRect by lazy { - Rect( - -timelineDrawable.intrinsicWidth / 2, - height / 2 - timelineDrawable.intrinsicHeight * 2, - timelineDrawable.intrinsicWidth * 2, - height / 2 + timelineDrawable.intrinsicHeight * 2 - ) - } - - constructor(context: Context?) : super(context) { - init() - } - - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { - init() - } - - /** - * 初始化画笔等 - */ - override fun init() { - highLightPaint.isAntiAlias = true - highLightPaint.color = highLightTextColor - - highLightPaint.textSize = DEFAULT_TEXT_SIZE * scalingFactor - highLightPaint.isFakeBoldText = true - - normalPaint.isAntiAlias = true - normalPaint.color = normalTextColor - normalPaint.textSize = DEFAULT_TEXT_SIZE * scalingFactor - - timeLinePaint.isAntiAlias = true - timeLineTextSize = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_SP, 11f, - context.resources.displayMetrics - ) - timeLinePaint.textSize = timeLineTextSize - timeLinePaint.color = timeLineTextColor - touchSlop = ViewConfiguration.get(context).scaledTouchSlop - - linePadding = DEFAULT_PADDING * scalingFactor - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - super.onSizeChanged(w, h, oldw, oldh) -// if (timelineRect == null) { //扩大点击区域 -// TIMELINE_DRAWABLE_RECT = Rect(-TIMELINE_DRAWABLE.intrinsicWidth / 2, -// height / 2 - TIMELINE_DRAWABLE.intrinsicHeight * 2, -// TIMELINE_DRAWABLE.intrinsicWidth * 2, -// height / 2 + TIMELINE_DRAWABLE.intrinsicHeight * 2) -// } - } - - @SuppressLint("DrawAllocation") - override fun onDraw(canvas: Canvas) { - if (lrcRows == null || lrcRows?.isEmpty() == true) { - //画默认的显示文字 - val textWidth = normalPaint.measureText(placeholder) - val textX = (width - textWidth) / 2 - normalPaint.alpha = 0xff - canvas.drawText(placeholder, textX, (height / 2).toFloat(), normalPaint) - return - } - val availableWidth = width - (paddingLeft + paddingRight) - rowY = (height / 2).toFloat() - lrcRows?.let { - for (i in it.indices) { - if (i == curRow) { //画高亮歌词 - drawLrcRow(canvas, highLightPaint, availableWidth, it[i]) - } else { //普通歌词 - drawLrcRow(canvas, normalPaint, availableWidth, it[i]) - } - } - } - - //画时间线和时间 - if (isDrawTimeLine) { -// final int timeLineOffsetY = -// mCurRow >= 0 && mLrcRows != null && mCurRow <= mLrcRows.size() - 1 ? -// mLrcRows.get(mCurRow).getTotalHeight() / 2 : -// 0; -// float y = getHeight() / 2 + getScrollY() + timeLineOffsetY ; - val y = height / 2 + scrollY + DEFAULT_SPACING_PADDING - val lrcRow = lrcRows?.get(curRow) - if (lrcRow != null) { - canvas.drawText( - lrcRow.timeStr, - width - timeLinePaint.measureText(lrcRow.timeStr) - 5, - y - 10, timeLinePaint - ) - } - - canvas.drawLine( - (timelineDrawable.intrinsicWidth + 10).toFloat(), - y, - width.toFloat(), - y, - timeLinePaint - ) - timelineDrawable.setBounds( - 0, - y.toInt() - timelineDrawable.intrinsicHeight / 2, - timelineDrawable.intrinsicWidth, - y.toInt() + timelineDrawable.intrinsicHeight / 2 - ) - timelineDrawable.draw(canvas) - } - } - - /** - * 分割绘制歌词 - */ - private fun drawLrcRow( - canvas: Canvas, - textPaint: TextPaint?, - availableWidth: Int, - lrcRow: LrcRow - ) { - drawText(canvas, textPaint, availableWidth, lrcRow.content) - if (lrcRow.hasTranslate()) { -// mRowY += DEFAULT_SPACING_PADDING; - drawText(canvas, textPaint, availableWidth, lrcRow.translate) - } - rowY += linePadding - } - - /** - * 分割绘制歌词 - */ - private fun drawText(canvas: Canvas, textPaint: TextPaint?, availableWidth: Int, text: String) { - val staticLayout = StaticLayout( - text, textPaint, availableWidth, - Layout.Alignment.ALIGN_CENTER, - DEFAULT_SPACING_MULTI, 0f, true - ) - val extra = if (staticLayout.lineCount > 1) DensityUtil.dip2px(context, 10f) else 0 - canvas.save() - canvas.translate(paddingLeft.toFloat(), rowY - staticLayout.height / 2 + extra) - staticLayout.draw(canvas) - canvas.restore() - rowY += staticLayout.height.toFloat() - } - - /** - * 是否可拖动歌词 - */ - private var canDrag = false - - /** - * 事件的第一次的y坐标 - */ - private var firstY = 0f - - /** - * 事件的上一次的y坐标 - */ - private var lastY = 0f - private var lastX = 0f - - /** - * 等待TimeLine - */ - private var timeLineWaiting = false - - /** - * 长按runnable - */ - private var longPressRunnable: Runnable? = LongPressRunnable() - private val timeLineDisableRunnable by lazy { - TimeLineRunnable() - } - - private val mHandler by lazy { - Handler(Looper.getMainLooper()) - } - - private inner class LongPressRunnable : Runnable { - override fun run() { - if (onLrcClickListener != null) { - onLrcClickListener?.onLongClick() - } - } - } - - private inner class TimeLineRunnable : Runnable { - override fun run() { - timeLineWaiting = false - isDrawTimeLine = false - invalidate() - } - } - - fun getLrcRows(): List? { - return lrcRows - } - - private fun hasLrc(): Boolean { - return lrcRows != null && lrcRows?.isNotEmpty() == true - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - - when (event.action) { - MotionEvent.ACTION_DOWN -> { - //没有歌词 - if (hasLrc()) { - firstY = event.rawY - lastX = event.rawX - if (timeLineWaiting) { - //点击定位图标 - if (timelineRect.contains(event.x.toInt(), event.y.toInt()) - && onSeekToListener != null && curRow != -1 - ) { - mHandler.removeCallbacks(timeLineDisableRunnable) - mHandler.post(timeLineDisableRunnable) - onSeekToListener?.onSeekTo(lrcRows?.get(curRow)?.time ?: 0) - return false - } - } - } - longPressRunnable = LongPressRunnable() - mHandler.postDelayed(longPressRunnable!!, ViewConfiguration.getLongPressTimeout().toLong()) - } - MotionEvent.ACTION_MOVE -> if (hasLrc()) { - if (!canDrag) { - if (abs(x = event.rawY - firstY) > touchSlop - && abs(x = event.rawY - firstY) > abs(x = event.rawX - lastX) - ) { - canDrag = true - isDrawTimeLine = true - scroller.forceFinished(true) - curFraction = 1f - } - lastY = event.rawY - } - if (canDrag) { - timeLineWaiting = false - longPressRunnable?.let { mHandler.removeCallbacks(it) } - val offset = event.rawY - lastY //偏移量 - if (scrollY - offset < 0) { - if (offset > 0) { -// offset = offset / 3; - } - } else if (scrollY - offset > totalHeight) { - if (offset < 0) { -// offset = offset / 3; - } - } - scrollBy(scrollX, (-offset).toInt()) - lastY = event.rawY - //根据滚动后的距离 查找歌词 -// int currentRow = (int) (getScrollY() / (mSizeForOtherLrc + mLinePadding)); - var currentRow = rowByScrollY - lrcRows?.let { lrcRows -> - currentRow = min(currentRow, lrcRows.size - 1) - currentRow = max(currentRow, 0) - seekTo(lrcRows[currentRow].time, false, false) - } - return true - } - lastY = event.rawY - } else { - longPressRunnable?.let { mHandler.removeCallbacks(it) } - } - MotionEvent.ACTION_UP -> if (!canDrag) { - if (longPressRunnable == null && onLrcClickListener != null) { - onLrcClickListener?.onClick() - } - longPressRunnable?.let { mHandler.removeCallbacks(it) } - longPressRunnable = null - } else { - //显示三秒TimeLine - mHandler.removeCallbacks(timeLineDisableRunnable) - mHandler.postDelayed(timeLineDisableRunnable, DURATION_TIME_LINE.toLong()) - timeLineWaiting = true - if (scrollY < 0) { - smoothScrollTo(0, DURATION_FOR_ACTION_UP) - } else if (scrollY > getScrollYByRow(curRow)) { - smoothScrollTo(getScrollYByRow(curRow), DURATION_FOR_ACTION_UP) - } - canDrag = false - // mIsDrawTimeLine = false; - invalidate() - } - MotionEvent.ACTION_CANCEL -> { - longPressRunnable?.let { mHandler.removeCallbacks(it) } - longPressRunnable = null - } - } - return true - } - - /** - * 为LrcView设置歌词List集合数据 - */ - override fun setLrcRows(lrcRows: List?) { - reset() - - this.lrcRows = lrcRows - this.lrcRows?.let { lrcRows -> - //计算每一行歌词所占的高度 -// for(LrcRow lrcRow : mLrcRows){ -// int height = 0; -// String combine = lrcRow.getContent() + (!TextUtils.isEmpty(lrcRow.getTranslate()) ? "\t" + lrcRow.getTranslate() : ""); -// String[] multiText = combine.split("\t"); -// for (String text : multiText) { -// float textWidth = mPaintForOtherLrc.measureText(text); -// int lineNumber = (int) Math.ceil(textWidth / getWidth()); -// -// height += lineNumber * mPaintForOtherLrc.getTextSize() + DEFAULT_SPACING_PADDING; -// } -// lrcRow.setHeight(height); -// } - calculateLrcRowHeight(lrcRows) - } - invalidate() - } - - private fun calculateLrcRowHeight(lrcRows: List) { - totalHeight = 0 - for (lrcRow in lrcRows) { - lrcRow.contentHeight = getSingleLineHeight(lrcRow.content) - if (lrcRow.hasTranslate()) { - lrcRow.translateHeight = getSingleLineHeight(lrcRow.translate) - } - lrcRow.totalHeight = lrcRow.translateHeight + lrcRow.contentHeight - totalHeight += lrcRow.totalHeight - } - } - - /** - * 获得单句歌词的高度,可能有多行 - */ - private fun getSingleLineHeight(text: String): Int { - val staticLayout = StaticLayout( - text, normalPaint, - width - paddingLeft - paddingRight, Layout.Alignment.ALIGN_CENTER, - DEFAULT_SPACING_MULTI, DEFAULT_SPACING_PADDING, true - ) - return staticLayout.height - } - - /** - * 当前高亮歌词的行号 - */ - private var curRow = -1 - - /** - * 到第n行所滚动过的距离 - */ - private fun getScrollYByRow(row: Int): Int { - if (lrcRows == null) { - return 0 - } - var scrollY = 0 - var i = 0 - - lrcRows?.let { - while (i < it.size && i < row) { - scrollY += (it[i].totalHeight + linePadding).toInt() - i++ - } - } - - return scrollY - } - - /** - * 根据当前行数计算滑动距离 - */ - private val rowByScrollY: Int - get() { - lrcRows?.let { lrcRows -> - var totalY = 0 - var line = 0 - while (line < lrcRows.size) { - totalY += (linePadding + lrcRows[line].totalHeight).toInt() - if (totalY >= scrollY) { - return line - } - line++ - } - return line - 1 - } - - return 0 - } - - private var offset = 0 - fun setOffset(offset: Int) { - this.offset = offset - invalidate() - } - - override fun seekTo(p: Int, fromSeekBar: Boolean, fromSeekBarByUser: Boolean) { - var progress = p - if (progress != 0) { - progress += offset - } - - lrcRows?.let { lrcRows -> - if (lrcRows.isEmpty()) { - return - } - //如果是由seekbar的进度改变触发 并且这时候处于拖动状态,则返回 - if (fromSeekBar && canDrag) { - return - } - //滑动处于等待的状态 - if (timeLineWaiting) { - return - } - for (i in lrcRows.indices.reversed()) { - if (progress >= lrcRows[i].time) { - if (curRow != i) { - curRow = i - if (fromSeekBarByUser) { - if (!scroller.isFinished) { - scroller.forceFinished(true) - } - scrollTo(scrollX, getScrollYByRow(curRow)) - } else { - smoothScrollTo(getScrollYByRow(curRow), DURATION_FOR_LRC_SCROLL) - } - //如果高亮歌词的宽度大于View的宽,就需要开启属性动画,让它水平滚动 -// float textWidth = mPaintForHighLightLrc.measureText(mLrcRows.get(mCurRow).getContent()); -// log("textWidth="+textWidth+"getWidth()=" + getWidth()); -// if(textWidth > getWidth()){ -// if(fromSeekBarByUser){ -// mScroller.forceFinished(true); -// } -// log("开始水平滚动歌词:" + mLrcRows.get(mCurRow).getContent()); -// startScrollLrc(getWidth() - textWidth, (long) (mLrcRows.get(mCurRow).getTotalTime() * 0.6)); -// } - invalidate() - } - break - } - } - } - - } - - /** - * 设置歌词的缩放比例 - */ - override fun setLrcScalingFactor(newFactor: Float) { - if (scalingFactor == newFactor) { - return - } - SPUtil.putValue( - context, - SPUtil.LYRIC_KEY.NAME, - SPUtil.LYRIC_KEY.LYRIC_FONT_SIZE, - newFactor.toString() - ) - scalingFactor = newFactor - highLightPaint.textSize = DEFAULT_TEXT_SIZE * scalingFactor - - normalPaint.textSize = DEFAULT_TEXT_SIZE * scalingFactor - - linePadding = DEFAULT_PADDING * scalingFactor - - lrcRows?.let { - calculateLrcRowHeight(it) - scrollTo(scrollX, getScrollYByRow(curRow)) - scroller.forceFinished(true) - } - - invalidate() - } - - /** - * 重置 - */ - override fun reset() { - if (!scroller.isFinished) { - scroller.forceFinished(true) - } - curRow = 0 - lrcRows = null - longPressRunnable?.let { mHandler.removeCallbacks(it) } - mHandler.removeCallbacks(timeLineDisableRunnable) - mHandler.post(timeLineDisableRunnable) - scrollTo(scrollX, 0) - invalidate() - } - - /** - * 平滑的移动到某处 - */ - private fun smoothScrollTo(dstY: Int, duration: Int) { - val oldScrollY = scrollY - val offset = dstY - oldScrollY - scroller.startScroll(scrollX, oldScrollY, scrollX, offset, duration) - invalidate() - } - - override fun computeScroll() { - if (!scroller.isFinished) { - if (scroller.computeScrollOffset()) { - val oldY = scrollY - val y = scroller.currY - if (oldY != y && !canDrag) { - scrollTo(scrollX, y) - } - curFraction = scroller.timePassed() * 3f / DURATION_FOR_LRC_SCROLL - curFraction = min(curFraction, 1f) - invalidate() - } - } - } - - private var onSeekToListener: OnSeekToListener? = null - fun setOnSeekToListener(onSeekToListener: OnSeekToListener) { - this.onSeekToListener = onSeekToListener - } - - fun setText(text: String) { - this.placeholder = text - reset() - } - - fun setText(@StringRes res: Int) { - this.placeholder = resources.getString(res) - setText(placeholder) - reset() - } - - interface OnSeekToListener { - fun onSeekTo(progress: Int) - } - - private var onLrcClickListener: OnLrcClickListener? = null - fun setOnLrcClickListener(mOnLrcClickListener: OnLrcClickListener?) { - this.onLrcClickListener = mOnLrcClickListener - } - - interface OnLrcClickListener { - fun onClick() - fun onLongClick() - } - - fun log(o: Any?) { - Timber.v("%s", o) - } - - /** - * 设置高亮歌词颜色 - */ - fun setHighLightColor(@ColorInt color: Int) { - highLightTextColor = color - highLightPaint.color = highLightTextColor - } - - /** - * 设置非高亮歌词颜色 - */ - fun setOtherColor(@ColorInt color: Int) { - normalTextColor = color - normalPaint.color = normalTextColor - } - - /** - * 设置时间线颜色 - */ - fun setTimeLineColor(@ColorInt color: Int) { - if (timeLineTextColor != color) { - timeLineTextColor = color - Theme.tintDrawable(timelineDrawable, color) - timeLinePaint.color = color - } - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - mHandler.removeCallbacksAndMessages(null) - } - - companion object { - val DEFAULT_TEXT_SIZE = DensityUtil.dip2px(App.context, 15f).toFloat() - - /** - * 歌词间默认的行距 - */ - val DEFAULT_PADDING = DensityUtil.dip2px(App.context, 10f).toFloat() - - /** - * 跨行歌词之间额外的行距 - */ - const val DEFAULT_SPACING_PADDING = 0f - /** DensityUtil.dip2px(App.getContext(),5) */ - /** - * 跨行歌词之间行距倍数 - */ - const val DEFAULT_SPACING_MULTI = 1f - - /** - * 高亮歌词的默认字体颜色 - */ - private const val DEFAULT_COLOR_FOR_HIGH_LIGHT_LRC = Color.BLACK - - /** - * 其他歌词的默认字体颜色 - */ - private const val DEFAULT_COLOR_FOR_OTHER_LRC = Color.GRAY - - /** - * 时间线默认大小 - */ - private const val DEFAULT_SIZE_FOR_TIMELINE = 35f - - /***移动一句歌词的持续时间 */ - private const val DURATION_FOR_LRC_SCROLL = 800 - - /***停止触摸时 如果View需要滚动 时的持续时间 */ - private const val DURATION_FOR_ACTION_UP = 400 - - /** - * 滑动后TimeLine显示的时间 - */ - private const val DURATION_TIME_LINE = 3000 - - } -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyric/LyricFetcher.kt b/app/src/main/java/remix/myplayer/lyric/LyricFetcher.kt deleted file mode 100644 index ac2e49523..000000000 --- a/app/src/main/java/remix/myplayer/lyric/LyricFetcher.kt +++ /dev/null @@ -1,129 +0,0 @@ -package remix.myplayer.lyric - -import io.reactivex.disposables.Disposable -import remix.myplayer.App -import remix.myplayer.bean.mp3.Song -import remix.myplayer.lyric.bean.LrcRow -import remix.myplayer.lyric.bean.LrcRow.Companion.LYRIC_EMPTY_ROW -import remix.myplayer.lyric.bean.LyricRowWrapper -import remix.myplayer.lyric.bean.LyricRowWrapper.Companion.LYRIC_WRAPPER_NO -import remix.myplayer.lyric.bean.LyricRowWrapper.Companion.LYRIC_WRAPPER_SEARCHING -import remix.myplayer.service.MusicService -import remix.myplayer.util.SPUtil -import timber.log.Timber -import java.lang.ref.WeakReference -import java.util.concurrent.CopyOnWriteArrayList - -/** - * Created by remix on 2019/2/6 - */ -class LyricFetcher(service: MusicService) { - - private val lrcRows = CopyOnWriteArrayList() - private val reference: WeakReference = WeakReference(service) - private var disposable: Disposable? = null - private var status = Status.SEARCHING - var song: Song = Song.EMPTY_SONG - var offset = 0 - private val lyricSearcher = LyricSearcher() - - - fun findCurrentLyric(): LyricRowWrapper { - val wrapper = LyricRowWrapper() - wrapper.status = status - val service = reference.get() - - when { - service == null || status == Status.NO -> { - return LYRIC_WRAPPER_NO - } - status == Status.SEARCHING -> { - return LYRIC_WRAPPER_SEARCHING - } - status == Status.NORMAL -> { - val song = service.currentSong - if (song == Song.EMPTY_SONG) { - Timber.v("歌曲异常") - return wrapper - } - val progress = service.progress + offset - - for (i in lrcRows.indices.reversed()) { - val lrcRow = lrcRows[i] - val interval = progress - lrcRow.time - if (i == 0 && interval < 0) { - //未开始歌唱前显示歌曲信息 - wrapper.lineOne = LrcRow("", 0, song.title) - wrapper.lineTwo = LrcRow("", 0, song.artist + " - " + song.album) - return wrapper - } else if (progress >= lrcRow.time) { - if (lrcRow.hasTranslate()) { - wrapper.lineOne = LrcRow(lrcRow) - wrapper.lineOne.content = lrcRow.content - wrapper.lineTwo = LrcRow(lrcRow) - wrapper.lineTwo.content = lrcRow.translate - } else { - wrapper.lineOne = lrcRow - wrapper.lineTwo = LrcRow(if (i + 1 < lrcRows.size) lrcRows[i + 1] else LYRIC_EMPTY_ROW) - } - return wrapper - } - } - return wrapper - } - else -> { - return LYRIC_WRAPPER_NO - } - } - } - - fun updateLyricRows(song: Song) { - this.song = song - - if (song == Song.EMPTY_SONG) { - status = Status.NO - lrcRows.clear() - return - } - - val id = song.id - - disposable?.dispose() - disposable = lyricSearcher.setSong(song) - .getLyricObservable() - .doOnSubscribe { - status = Status.SEARCHING - } - .subscribe({ -// Timber.v("updateLyricRows, lrc: $it") - if (id == song.id) { - status = Status.NORMAL - offset = SPUtil.getValue(App.context, SPUtil.LYRIC_OFFSET_KEY.NAME, id.toString(), 0) - lrcRows.clear() - lrcRows.addAll(it) - } else { - lrcRows.clear() - status = Status.NO - } - }, { throwable -> - Timber.v(throwable) - if (id == song.id) { - status = Status.NO - lrcRows.clear() - } - }) - } - - fun dispose() { - disposable?.dispose() - } - - enum class Status { - NO, SEARCHING, NORMAL - } - - companion object { - const val LYRIC_FIND_INTERVAL = 400L - } - -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyric/LyricSearcher.kt b/app/src/main/java/remix/myplayer/lyric/LyricSearcher.kt index f5390b543..c5ba92333 100644 --- a/app/src/main/java/remix/myplayer/lyric/LyricSearcher.kt +++ b/app/src/main/java/remix/myplayer/lyric/LyricSearcher.kt @@ -1,481 +1,481 @@ -package remix.myplayer.lyric - -import android.net.Uri -import android.provider.MediaStore -import android.text.TextUtils -import android.util.Base64 -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.functions.Function -import org.jaudiotagger.audio.AudioFileIO -import org.jaudiotagger.tag.FieldKey -import remix.myplayer.App -import remix.myplayer.bean.misc.LyricPriority -import remix.myplayer.bean.mp3.Song -import remix.myplayer.lyric.bean.LrcRow -import remix.myplayer.misc.cache.DiskCache -import remix.myplayer.request.network.HttpClient -import remix.myplayer.util.* -import timber.log.Timber -import java.io.* -import java.nio.charset.Charset -import java.util.* - -/** - * Created by Remix on 2015/12/7. - */ - -/** - * 根据歌曲名和歌手名 搜索歌词并解析成固定格式 - */ -class LyricSearcher { - private var song: Song = Song.EMPTY_SONG - private val lrcParser: ILrcParser - private var displayName: String? = null - private var cacheKey: String? = null - private var searchKey: String? = null - - init { - lrcParser = DefaultLrcParser() - } - - private fun parse() { - try { - if (!TextUtils.isEmpty(song.displayName)) { - val temp = song.displayName - displayName = if (temp.indexOf('.') > 0) temp.substring(0, temp.lastIndexOf('.')) else temp - } - searchKey = getLyricSearchKey(song) - } catch (e: Exception) { - Timber.v(e) - displayName = song.title - } - } - - fun setSong(song: Song): LyricSearcher { - this.song = song - parse() - return this - } - - /** - * 发送请求并解析歌词 - * - * @return 歌词 - */ - fun getLyricObservable(uri: Uri, clearCache: Boolean): Observable> { - if (song == Song.EMPTY_SONG) { - return Observable.error(Throwable("empty song")) - } - - val type = SPUtil.getValue(App.context, SPUtil.LYRIC_KEY.NAME, song.id, SPUtil.LYRIC_KEY.LYRIC_DEFAULT) - - val observable = when (type) { - SPUtil.LYRIC_KEY.LYRIC_IGNORE -> { - Timber.v("ignore lyric") - return Observable.error(Throwable("ignore lyric")) - } - SPUtil.LYRIC_KEY.LYRIC_EMBEDDED -> { - getEmbeddedObservable() - } - SPUtil.LYRIC_KEY.LYRIC_LOCAL -> { - getLocalObservable() - } - SPUtil.LYRIC_KEY.LYRIC_KUGOU -> { - getKuGouObservable() - } - SPUtil.LYRIC_KEY.LYRIC_NETEASE -> { - getNeteaseObservable() - } - SPUtil.LYRIC_KEY.LYRIC_QQ -> { - getQQObservable() - } - SPUtil.LYRIC_KEY.LYRIC_MANUAL -> { - getManualObservable(uri) - } - SPUtil.LYRIC_KEY.LYRIC_DEFAULT -> { - //默认优先级排序 本地-内嵌-酷狗-网易-QQ-忽略 - - val priority = Gson().fromJson>(SPUtil.getValue(App.context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.PRIORITY_LYRIC, SPUtil.LYRIC_KEY.DEFAULT_PRIORITY), - object : TypeToken>() {}.type) - if (priority.firstOrNull() == LyricPriority.IGNORE) { - return Observable.error(Throwable("ignore lyric")) - } - - val observables = mutableListOf>>() - priority.forEach { - when (it.priority) { - LyricPriority.KUGOU.priority -> observables.add(getKuGouObservable()) - LyricPriority.NETEASE.priority -> observables.add(getNeteaseObservable()) - LyricPriority.QQ.priority -> observables.add(getQQObservable()) - LyricPriority.LOCAL.priority -> observables.add(getLocalObservable()) - LyricPriority.EMBEDDED.priority -> observables.add(getEmbeddedObservable()) - LyricPriority.IGNORE.priority -> observables.add(Observable.create { emitter -> - emitter.onError(Throwable("ignore lyric")) - }) - } - } - Observable.concat(observables).firstOrError().toObservable() - } - else -> { - Observable.error(Throwable("unknown type")) - } - } - - return if (isTypeAvailable(type)) Observable.concat(getCacheObservable(), observable) - .firstOrError() - .toObservable() - .doOnError { - Timber.w(it) - } - .doOnSubscribe { - cacheKey = Util.hashKeyForDisk(song.id.toString() + "-" + - (if (!TextUtils.isEmpty(song.artist)) song.artist else "") + "-" + - if (!TextUtils.isEmpty(song.title)) song.title else "") - Timber.v("CacheKey: $cacheKey SearchKey: $searchKey") - if (clearCache) { - Timber.v("clearCache") - DiskCache.getLrcDiskCache().remove(cacheKey) - } - }.compose(RxUtil.applyScheduler()) - else - observable - } - - private fun isTypeAvailable(type: Int): Boolean { - return type != SPUtil.LYRIC_KEY.LYRIC_IGNORE - } - - /** - * 根据歌词id,发送请求并解析歌词 - * - * @return 歌词 - */ - fun getLyricObservable(): Observable> { - return getLyricObservable(Uri.EMPTY, false) - } - - /** - * 内嵌歌词 - * - * @return - */ - private fun getEmbeddedObservable(): Observable> { - return Observable.create { e -> - val audioFile = if (song.isLocal()) AudioFileIO.read(File(song.data)) else null - val lyric = try { - audioFile?.tag?.getFirst(FieldKey.LYRICS) - } catch (e: Exception) { - "" - } - if (!lyric.isNullOrEmpty()) { - e.onNext(lrcParser.getLrcRows(getBufferReader(lyric.toByteArray(UTF_8)), - true, cacheKey, searchKey)) - Timber.v("EmbeddedLyric") - } - e.onComplete() - } - } - - /** - * 缓存 - */ - private fun getCacheObservable(): Observable> { - return Observable.create { e -> - DiskCache.getLrcDiskCache().get(cacheKey)?.run { - BufferedReader(InputStreamReader(getInputStream(0))).use { - it.readLine().run { - e.onNext(Gson().fromJson(this, object : TypeToken>() {}.type)) - Timber.v("CacheLyric") - } - } - } - e.onComplete() - } - } - - private val isCN: Boolean - get() = "zh".equals(Locale.getDefault().language, ignoreCase = true) - - /** - * 搜索本地所有歌词文件 - */ - private fun getLocalLyricPath(): String? { - var path = "" - //没有设置歌词路径 搜索所有可能的歌词文件 - App.context.contentResolver.query(MediaStore.Files.getContentUri("external"), null, - MediaStore.Files.FileColumns.DATA + " like ? or " + - MediaStore.Files.FileColumns.DATA + " like ? or " + - MediaStore.Files.FileColumns.DATA + " like ? or " + - MediaStore.Files.FileColumns.DATA + " like ? or " + - MediaStore.Files.FileColumns.DATA + " like ? or " + - MediaStore.Files.FileColumns.DATA + " like ?", - getLocalSearchKey(), - null) - .use { filesCursor -> - if (filesCursor == null) { - return "" - } - while (filesCursor.moveToNext()) { - val file = File(filesCursor.getString(filesCursor.getColumnIndex(MediaStore.Files.FileColumns.DATA))) - Timber.v("file: %s", file.absolutePath) - if (file.exists() && file.isFile && file.canRead()) { - path = file.absolutePath - break - } - } - return path - } - } - - /** - * @param searchPath 设置的本地歌词搜索路径 - * 本地歌词搜索的关键字 - * artist-displayName.lrc - * displayName.lrc - * title.lrc - * title-artist.lrc - * displayname-artist.lrc - * artist-title.lrc - */ - private fun getLocalSearchKey(searchPath: String? = null): Array { - return arrayOf("%${song.artist}%$displayName$SUFFIX_LYRIC", - "%$displayName$SUFFIX_LYRIC", - "%${song.title}$SUFFIX_LYRIC", - "%${song.title}%${song.artist}$SUFFIX_LYRIC", - "%${song.displayName}%${song.artist}$SUFFIX_LYRIC", - "%${song.artist}%${song.title}$SUFFIX_LYRIC") - } - - /** - * 网络或者本地歌词 - * - * @param type - * @return - */ - @Deprecated("") - private fun getContentObservable(type: Int): Observable> { - val networkObservable = getNetworkObservable(type) - val localObservable = getLocalObservable() - val onlineFirst = SPUtil.getValue(App.context, SPUtil.SETTING_KEY.NAME, SPUtil.SETTING_KEY.ONLINE_LYRIC_FIRST, false) - return Observable.concat(if (onlineFirst) networkObservable else localObservable, if (onlineFirst) localObservable else networkObservable).firstOrError().toObservable() - } - - /** - * 手动设置歌词 - */ - private fun getManualObservable(uri: Uri): Observable> { - return Observable.create { e -> - //手动设置的歌词 - Timber.v("ManualLyric") - e.onNext( - lrcParser.getLrcRows( - BufferedReader( - InputStreamReader( - App.context.contentResolver.openInputStream( - uri - ) - ) - ), true, cacheKey, searchKey - ) - ) - e.onComplete() - } - } - - - /** - * 本地歌词 - * - * @param type - * @return - */ - private fun getLocalObservable(): Observable> { - return Observable - .create { emitter -> - val path = getLocalLyricPath() - if (path != null && path.isNotEmpty()) { - Timber.v("LocalLyric") - emitter.onNext(lrcParser.getLrcRows(getBufferReader(path), true, cacheKey, searchKey)) - } - emitter.onComplete() - } - } - - /** - * 网易歌词 - */ - private fun getNeteaseObservable(): Observable> { - return HttpClient.searchNeteaseSong(searchKey, 0, 1) - .flatMap { - HttpClient.searchNeteaseLyric(it.result?.songs?.get(0)?.id ?: 0) - } - .map { lrcResponse -> - val combine = lrcParser.getLrcRows(getBufferReader(lrcResponse.lrc?.lyric?.toByteArray() - ?: "".toByteArray()), false, cacheKey, searchKey) - if (isCN && lrcResponse.tlyric != null && !lrcResponse.tlyric.lyric.isNullOrEmpty()) { - val translate = lrcParser.getLrcRows(getBufferReader(lrcResponse.tlyric.lyric.toByteArray()), false, cacheKey, searchKey) - if (translate.isNotEmpty()) { - for (i in translate.indices) { - for (j in combine.indices) { - if (translate[i].time == combine[j].time) { - combine[j].translate = translate[i].content - break - } - } - } - } - } - Timber.v("NeteaseLyric") - lrcParser.saveLrcRows(combine, cacheKey, searchKey) - combine - } - .toObservable() - .onErrorResumeNext(Function { - Timber.w("search netease lyric failed: ${it.message}") - Observable.empty() - }) - } - - /** - * 酷狗歌词 - */ - private fun getKuGouObservable(): Observable> { - //酷狗歌词 - return HttpClient.searchKuGou(searchKey, song.duration) - .flatMap { searchResponse -> - if (searchResponse.candidates.isNotEmpty() && song.title.equals(searchResponse.candidates[0].song, true)) { - HttpClient.searchKuGouLyric( - searchResponse.candidates[0].id, - searchResponse.candidates[0].accesskey) - .map { lrcResponse -> - Timber.v("KugouLyric") - val rows = lrcParser.getLrcRows(getBufferReader(Base64.decode(lrcResponse.content, Base64.DEFAULT)), true, cacheKey, searchKey) - rows.forEach { - it.content = Util.htmlToText(it.content) - } - rows - } - } else { - Single.error(Throwable("no kugou lyric")) - } - } - .toObservable() - .onErrorResumeNext(Function { - Timber.w("search kugou lyric failed: ${it.message}") - Observable.empty() - }) - } - - /** - * QQ歌词 - */ - private fun getQQObservable(): Observable> { - return HttpClient.searchQQ(searchKey) - .flatMap { searchResponse -> - if (song.title.equals(searchResponse.data.song.list[0].songname, true)) { - HttpClient.searchQQLyric(searchResponse.data.song.list[0].songmid) - .map { lrcResponse -> - val combine = lrcParser.getLrcRows(getBufferReader(lrcResponse.lyric.toByteArray()), false, cacheKey, searchKey) - combine.forEach { - it.content = Util.htmlToText(it.content) - } - if (lrcResponse.trans.isNotEmpty()) { - val translate = lrcParser.getLrcRows(getBufferReader(lrcResponse.trans.toByteArray()), false, cacheKey, searchKey) - if (isCN && translate.isNotEmpty()) { - translate.forEach { - if (it.content.isNotEmpty() && it.content != "//") { - for (i in combine.indices) { - if (it.time == combine[i].time) { - combine[i].translate = Util.htmlToText(it.content) - break - } - } - } - } - } - } - Timber.v("QQLyric") - lrcParser.saveLrcRows(combine, cacheKey, searchKey) - combine - } - } else { - Single.error(Throwable("no qq lyric")) - } - } - .toObservable() - .onErrorResumeNext(Function { - Timber.w("search qq lyric failed: ${it.message}") - Observable.empty() - }) - } - - /** - * 在线歌词 - * - * @param type - * @return - */ - @Deprecated("") - private fun getNetworkObservable(type: Int): Observable> { - var newType = type - - if (TextUtils.isEmpty(searchKey)) { - return Observable.error(Throwable("no available key")) - } - if (newType == SPUtil.LYRIC_KEY.LYRIC_DEFAULT) - newType = SPUtil.LYRIC_KEY.LYRIC_NETEASE - return if (newType == SPUtil.LYRIC_KEY.LYRIC_KUGOU) { - //酷狗歌词 - getKuGouObservable() - } else { - //网易歌词 - getNeteaseObservable() - } - } - - private fun isTranslateCanUse(translate: String): Boolean { - return !TextUtils.isEmpty(translate) && !translate.startsWith("词") && !translate.startsWith("曲") - } - - /** - * 获得搜索歌词的关键字 - * - * @param song - * @return - */ - private fun getLyricSearchKey(song: Song?): String { - if (song == null) - return "" - val isTitleAvailable = !ImageUriUtil.isSongNameUnknownOrEmpty(song.title) - val isAlbumAvailable = !ImageUriUtil.isAlbumNameUnknownOrEmpty(song.album) - val isArtistAvailable = !ImageUriUtil.isArtistNameUnknownOrEmpty(song.artist) - - //歌曲名合法 - return if (isTitleAvailable) { - when { - isArtistAvailable -> song.artist + "-" + song.title //艺术家合法 - isAlbumAvailable -> //专辑名合法 - song.album + "-" + song.title - else -> song.title - } - } else "" - } - - @Throws(FileNotFoundException::class, UnsupportedEncodingException::class) - private fun getBufferReader(path: String): BufferedReader { - return BufferedReader(InputStreamReader(FileInputStream(path), LyricUtil.getCharset(path))) - } - - private fun getBufferReader(bytes: ByteArray): BufferedReader { - return BufferedReader(InputStreamReader(ByteArrayInputStream(bytes), UTF_8)) - } - - companion object { - private const val TAG = "LyricSearcher" - private const val SUFFIX_LYRIC = ".lrc" - private val UTF_8 = Charset.forName("UTF-8") - } -} +//package remix.myplayer.lyric +// +//import android.net.Uri +//import android.provider.MediaStore +//import android.text.TextUtils +//import android.util.Base64 +//import com.google.gson.Gson +//import com.google.gson.reflect.TypeToken +//import io.reactivex.Observable +//import io.reactivex.Single +//import io.reactivex.functions.Function +//import org.jaudiotagger.audio.AudioFileIO +//import org.jaudiotagger.tag.FieldKey +//import remix.myplayer.App +//import remix.myplayer.bean.misc.LyricPriority +//import remix.myplayer.bean.mp3.Song +//import remix.myplayer.lyric.bean.LrcRow +//import remix.myplayer.misc.cache.DiskCache +//import remix.myplayer.request.network.HttpClient +//import remix.myplayer.util.* +//import timber.log.Timber +//import java.io.* +//import java.nio.charset.Charset +//import java.util.* +// +///** +// * Created by Remix on 2015/12/7. +// */ +// +///** +// * 根据歌曲名和歌手名 搜索歌词并解析成固定格式 +// */ +//class LyricSearcher { +// private var song: Song = Song.EMPTY_SONG +// private val lrcParser: ILrcParser +// private var displayName: String? = null +// private var cacheKey: String? = null +// private var searchKey: String? = null +// +// init { +// lrcParser = DefaultLrcParser() +// } +// +// private fun parse() { +// try { +// if (!TextUtils.isEmpty(song.displayName)) { +// val temp = song.displayName +// displayName = if (temp.indexOf('.') > 0) temp.substring(0, temp.lastIndexOf('.')) else temp +// } +// searchKey = getLyricSearchKey(song) +// } catch (e: Exception) { +// Timber.v(e) +// displayName = song.title +// } +// } +// +// fun setSong(song: Song): LyricSearcher { +// this.song = song +// parse() +// return this +// } +// +// /** +// * 发送请求并解析歌词 +// * +// * @return 歌词 +// */ +// fun getLyricObservable(uri: Uri, clearCache: Boolean): Observable> { +// if (song == Song.EMPTY_SONG) { +// return Observable.error(Throwable("empty song")) +// } +// +// val type = SPUtil.getValue(App.context, SPUtil.LYRIC_KEY.NAME, song.id, SPUtil.LYRIC_KEY.LYRIC_DEFAULT) +// +// val observable = when (type) { +// SPUtil.LYRIC_KEY.LYRIC_IGNORE -> { +// Timber.v("ignore lyric") +// return Observable.error(Throwable("ignore lyric")) +// } +// SPUtil.LYRIC_KEY.LYRIC_EMBEDDED -> { +// getEmbeddedObservable() +// } +// SPUtil.LYRIC_KEY.LYRIC_LOCAL -> { +// getLocalObservable() +// } +// SPUtil.LYRIC_KEY.LYRIC_KUGOU -> { +// getKuGouObservable() +// } +// SPUtil.LYRIC_KEY.LYRIC_NETEASE -> { +// getNeteaseObservable() +// } +// SPUtil.LYRIC_KEY.LYRIC_QQ -> { +// getQQObservable() +// } +// SPUtil.LYRIC_KEY.LYRIC_MANUAL -> { +// getManualObservable(uri) +// } +// SPUtil.LYRIC_KEY.LYRIC_DEFAULT -> { +// //默认优先级排序 本地-内嵌-酷狗-网易-QQ-忽略 +// +// val priority = Gson().fromJson>(SPUtil.getValue(App.context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.PRIORITY_LYRIC, SPUtil.LYRIC_KEY.DEFAULT_PRIORITY), +// object : TypeToken>() {}.type) +// if (priority.firstOrNull() == LyricPriority.IGNORE) { +// return Observable.error(Throwable("ignore lyric")) +// } +// +// val observables = mutableListOf>>() +// priority.forEach { +// when (it.priority) { +// LyricPriority.KUGOU.priority -> observables.add(getKuGouObservable()) +// LyricPriority.NETEASE.priority -> observables.add(getNeteaseObservable()) +// LyricPriority.QQ.priority -> observables.add(getQQObservable()) +// LyricPriority.LOCAL.priority -> observables.add(getLocalObservable()) +// LyricPriority.EMBEDDED.priority -> observables.add(getEmbeddedObservable()) +// LyricPriority.IGNORE.priority -> observables.add(Observable.create { emitter -> +// emitter.onError(Throwable("ignore lyric")) +// }) +// } +// } +// Observable.concat(observables).firstOrError().toObservable() +// } +// else -> { +// Observable.error(Throwable("unknown type")) +// } +// } +// +// return if (isTypeAvailable(type)) Observable.concat(getCacheObservable(), observable) +// .firstOrError() +// .toObservable() +// .doOnError { +// Timber.w(it) +// } +// .doOnSubscribe { +// cacheKey = Util.hashKeyForDisk(song.id.toString() + "-" + +// (if (!TextUtils.isEmpty(song.artist)) song.artist else "") + "-" + +// if (!TextUtils.isEmpty(song.title)) song.title else "") +// Timber.v("CacheKey: $cacheKey SearchKey: $searchKey") +// if (clearCache) { +// Timber.v("clearCache") +// DiskCache.getLrcDiskCache().remove(cacheKey) +// } +// }.compose(RxUtil.applyScheduler()) +// else +// observable +// } +// +// private fun isTypeAvailable(type: Int): Boolean { +// return type != SPUtil.LYRIC_KEY.LYRIC_IGNORE +// } +// +// /** +// * 根据歌词id,发送请求并解析歌词 +// * +// * @return 歌词 +// */ +// fun getLyricObservable(): Observable> { +// return getLyricObservable(Uri.EMPTY, false) +// } +// +// /** +// * 内嵌歌词 +// * +// * @return +// */ +// private fun getEmbeddedObservable(): Observable> { +// return Observable.create { e -> +// val audioFile = if (song.isLocal()) AudioFileIO.read(File(song.data)) else null +// val lyric = try { +// audioFile?.tag?.getFirst(FieldKey.LYRICS) +// } catch (e: Exception) { +// "" +// } +// if (!lyric.isNullOrEmpty()) { +// e.onNext(lrcParser.getLrcRows(getBufferReader(lyric.toByteArray(UTF_8)), +// true, cacheKey, searchKey)) +// Timber.v("EmbeddedLyric") +// } +// e.onComplete() +// } +// } +// +// /** +// * 缓存 +// */ +// private fun getCacheObservable(): Observable> { +// return Observable.create { e -> +// DiskCache.getLrcDiskCache().get(cacheKey)?.run { +// BufferedReader(InputStreamReader(getInputStream(0))).use { +// it.readLine().run { +// e.onNext(Gson().fromJson(this, object : TypeToken>() {}.type)) +// Timber.v("CacheLyric") +// } +// } +// } +// e.onComplete() +// } +// } +// +// private val isCN: Boolean +// get() = "zh".equals(Locale.getDefault().language, ignoreCase = true) +// +// /** +// * 搜索本地所有歌词文件 +// */ +// private fun getLocalLyricPath(): String? { +// var path = "" +// //没有设置歌词路径 搜索所有可能的歌词文件 +// App.context.contentResolver.query(MediaStore.Files.getContentUri("external"), null, +// MediaStore.Files.FileColumns.DATA + " like ? or " + +// MediaStore.Files.FileColumns.DATA + " like ? or " + +// MediaStore.Files.FileColumns.DATA + " like ? or " + +// MediaStore.Files.FileColumns.DATA + " like ? or " + +// MediaStore.Files.FileColumns.DATA + " like ? or " + +// MediaStore.Files.FileColumns.DATA + " like ?", +// getLocalSearchKey(), +// null) +// .use { filesCursor -> +// if (filesCursor == null) { +// return "" +// } +// while (filesCursor.moveToNext()) { +// val file = File(filesCursor.getString(filesCursor.getColumnIndex(MediaStore.Files.FileColumns.DATA))) +// Timber.v("file: %s", file.absolutePath) +// if (file.exists() && file.isFile && file.canRead()) { +// path = file.absolutePath +// break +// } +// } +// return path +// } +// } +// +// /** +// * @param searchPath 设置的本地歌词搜索路径 +// * 本地歌词搜索的关键字 +// * artist-displayName.lrc +// * displayName.lrc +// * title.lrc +// * title-artist.lrc +// * displayname-artist.lrc +// * artist-title.lrc +// */ +// private fun getLocalSearchKey(searchPath: String? = null): Array { +// return arrayOf("%${song.artist}%$displayName$SUFFIX_LYRIC", +// "%$displayName$SUFFIX_LYRIC", +// "%${song.title}$SUFFIX_LYRIC", +// "%${song.title}%${song.artist}$SUFFIX_LYRIC", +// "%${song.displayName}%${song.artist}$SUFFIX_LYRIC", +// "%${song.artist}%${song.title}$SUFFIX_LYRIC") +// } +// +// /** +// * 网络或者本地歌词 +// * +// * @param type +// * @return +// */ +// @Deprecated("") +// private fun getContentObservable(type: Int): Observable> { +// val networkObservable = getNetworkObservable(type) +// val localObservable = getLocalObservable() +// val onlineFirst = SPUtil.getValue(App.context, SPUtil.SETTING_KEY.NAME, SPUtil.SETTING_KEY.ONLINE_LYRIC_FIRST, false) +// return Observable.concat(if (onlineFirst) networkObservable else localObservable, if (onlineFirst) localObservable else networkObservable).firstOrError().toObservable() +// } +// +// /** +// * 手动设置歌词 +// */ +// private fun getManualObservable(uri: Uri): Observable> { +// return Observable.create { e -> +// //手动设置的歌词 +// Timber.v("ManualLyric") +// e.onNext( +// lrcParser.getLrcRows( +// BufferedReader( +// InputStreamReader( +// App.context.contentResolver.openInputStream( +// uri +// ) +// ) +// ), true, cacheKey, searchKey +// ) +// ) +// e.onComplete() +// } +// } +// +// +// /** +// * 本地歌词 +// * +// * @param type +// * @return +// */ +// private fun getLocalObservable(): Observable> { +// return Observable +// .create { emitter -> +// val path = getLocalLyricPath() +// if (path != null && path.isNotEmpty()) { +// Timber.v("LocalLyric") +// emitter.onNext(lrcParser.getLrcRows(getBufferReader(path), true, cacheKey, searchKey)) +// } +// emitter.onComplete() +// } +// } +// +// /** +// * 网易歌词 +// */ +// private fun getNeteaseObservable(): Observable> { +// return HttpClient.searchNeteaseSong(searchKey, 0, 1) +// .flatMap { +// HttpClient.searchNeteaseLyric(it.result?.songs?.get(0)?.id ?: 0) +// } +// .map { lrcResponse -> +// val combine = lrcParser.getLrcRows(getBufferReader(lrcResponse.lrc?.lyric?.toByteArray() +// ?: "".toByteArray()), false, cacheKey, searchKey) +// if (isCN && lrcResponse.tlyric != null && !lrcResponse.tlyric.lyric.isNullOrEmpty()) { +// val translate = lrcParser.getLrcRows(getBufferReader(lrcResponse.tlyric.lyric.toByteArray()), false, cacheKey, searchKey) +// if (translate.isNotEmpty()) { +// for (i in translate.indices) { +// for (j in combine.indices) { +// if (translate[i].time == combine[j].time) { +// combine[j].translate = translate[i].content +// break +// } +// } +// } +// } +// } +// Timber.v("NeteaseLyric") +// lrcParser.saveLrcRows(combine, cacheKey, searchKey) +// combine +// } +// .toObservable() +// .onErrorResumeNext(Function { +// Timber.w("search netease lyric failed: ${it.message}") +// Observable.empty() +// }) +// } +// +// /** +// * 酷狗歌词 +// */ +// private fun getKuGouObservable(): Observable> { +// //酷狗歌词 +// return HttpClient.searchKuGou(searchKey, song.duration) +// .flatMap { searchResponse -> +// if (searchResponse.candidates.isNotEmpty() && song.title.equals(searchResponse.candidates[0].song, true)) { +// HttpClient.searchKuGouLyric( +// searchResponse.candidates[0].id, +// searchResponse.candidates[0].accesskey) +// .map { lrcResponse -> +// Timber.v("KugouLyric") +// val rows = lrcParser.getLrcRows(getBufferReader(Base64.decode(lrcResponse.content, Base64.DEFAULT)), true, cacheKey, searchKey) +// rows.forEach { +// it.content = Util.htmlToText(it.content) +// } +// rows +// } +// } else { +// Single.error(Throwable("no kugou lyric")) +// } +// } +// .toObservable() +// .onErrorResumeNext(Function { +// Timber.w("search kugou lyric failed: ${it.message}") +// Observable.empty() +// }) +// } +// +// /** +// * QQ歌词 +// */ +// private fun getQQObservable(): Observable> { +// return HttpClient.searchQQ(searchKey) +// .flatMap { searchResponse -> +// if (song.title.equals(searchResponse.data.song.list[0].songname, true)) { +// HttpClient.searchQQLyric(searchResponse.data.song.list[0].songmid) +// .map { lrcResponse -> +// val combine = lrcParser.getLrcRows(getBufferReader(lrcResponse.lyric.toByteArray()), false, cacheKey, searchKey) +// combine.forEach { +// it.content = Util.htmlToText(it.content) +// } +// if (lrcResponse.trans.isNotEmpty()) { +// val translate = lrcParser.getLrcRows(getBufferReader(lrcResponse.trans.toByteArray()), false, cacheKey, searchKey) +// if (isCN && translate.isNotEmpty()) { +// translate.forEach { +// if (it.content.isNotEmpty() && it.content != "//") { +// for (i in combine.indices) { +// if (it.time == combine[i].time) { +// combine[i].translate = Util.htmlToText(it.content) +// break +// } +// } +// } +// } +// } +// } +// Timber.v("QQLyric") +// lrcParser.saveLrcRows(combine, cacheKey, searchKey) +// combine +// } +// } else { +// Single.error(Throwable("no qq lyric")) +// } +// } +// .toObservable() +// .onErrorResumeNext(Function { +// Timber.w("search qq lyric failed: ${it.message}") +// Observable.empty() +// }) +// } +// +// /** +// * 在线歌词 +// * +// * @param type +// * @return +// */ +// @Deprecated("") +// private fun getNetworkObservable(type: Int): Observable> { +// var newType = type +// +// if (TextUtils.isEmpty(searchKey)) { +// return Observable.error(Throwable("no available key")) +// } +// if (newType == SPUtil.LYRIC_KEY.LYRIC_DEFAULT) +// newType = SPUtil.LYRIC_KEY.LYRIC_NETEASE +// return if (newType == SPUtil.LYRIC_KEY.LYRIC_KUGOU) { +// //酷狗歌词 +// getKuGouObservable() +// } else { +// //网易歌词 +// getNeteaseObservable() +// } +// } +// +// private fun isTranslateCanUse(translate: String): Boolean { +// return !TextUtils.isEmpty(translate) && !translate.startsWith("词") && !translate.startsWith("曲") +// } +// +// /** +// * 获得搜索歌词的关键字 +// * +// * @param song +// * @return +// */ +// private fun getLyricSearchKey(song: Song?): String { +// if (song == null) +// return "" +// val isTitleAvailable = !ImageUriUtil.isSongNameUnknownOrEmpty(song.title) +// val isAlbumAvailable = !ImageUriUtil.isAlbumNameUnknownOrEmpty(song.album) +// val isArtistAvailable = !ImageUriUtil.isArtistNameUnknownOrEmpty(song.artist) +// +// //歌曲名合法 +// return if (isTitleAvailable) { +// when { +// isArtistAvailable -> song.artist + "-" + song.title //艺术家合法 +// isAlbumAvailable -> //专辑名合法 +// song.album + "-" + song.title +// else -> song.title +// } +// } else "" +// } +// +// @Throws(FileNotFoundException::class, UnsupportedEncodingException::class) +// private fun getBufferReader(path: String): BufferedReader { +// return BufferedReader(InputStreamReader(FileInputStream(path), LyricUtil.getCharset(path))) +// } +// +// private fun getBufferReader(bytes: ByteArray): BufferedReader { +// return BufferedReader(InputStreamReader(ByteArrayInputStream(bytes), UTF_8)) +// } +// +// companion object { +// private const val TAG = "LyricSearcher" +// private const val SUFFIX_LYRIC = ".lrc" +// private val UTF_8 = Charset.forName("UTF-8") +// } +//} diff --git a/app/src/main/java/remix/myplayer/lyric/UpdateLyricThread.java b/app/src/main/java/remix/myplayer/lyric/UpdateLyricThread.java deleted file mode 100644 index 1dae2815a..000000000 --- a/app/src/main/java/remix/myplayer/lyric/UpdateLyricThread.java +++ /dev/null @@ -1,138 +0,0 @@ -//package remix.myplayer.lyric; -// -//import io.reactivex.disposables.Disposable; -//import java.lang.ref.WeakReference; -//import java.util.List; -//import remix.myplayer.App; -//import remix.myplayer.R; -//import remix.myplayer.bean.mp3.Song; -//import remix.myplayer.lyric.bean.LrcRow; -//import remix.myplayer.lyric.bean.LyricRowWrapper; -//import remix.myplayer.service.MusicService; -//import remix.myplayer.util.SPUtil; -//import timber.log.Timber; -// -//public abstract class UpdateLyricThread extends Thread { -// -// public static final String TAG = UpdateLyricThread.class.getSimpleName(); -// public static final LrcRow LYRIC_EMPTY_ROW = new LrcRow("", 0, ""); -// public static final LrcRow LYRIC_NO_ROW = new LrcRow("", 0, -// App.getContext().getString(R.string.no_lrc)); -// public static final LrcRow LYRIC_SEARCHING_ROW = new LrcRow("", 0, -// App.getContext().getString(R.string.searching)); -// public static final int LRC_INTERVAL = 400; -// -// private volatile List mLrcRows; -// private Disposable mDisposable; -// private WeakReference mReference; -// private Song mSong; -// private Status mStatus = Status.SEARCHING; -// private int mOffset = 0; -// private LyricSearcher mLyricSearcher = new LyricSearcher(); -// -// public UpdateLyricThread(MusicService service) { -// mReference = new WeakReference<>(service); -// setSongAndGetLyricRows(mReference.get().getCurrentSong()); -// } -// -// private void updateLrcRows() { -// if (mSong == null) { -// mStatus = Status.NO; -// mLrcRows = null; -// return; -// } -// final int id = mSong.getId(); -// if (mDisposable != null && !mDisposable.isDisposed()) { -// mDisposable.dispose(); -// } -// mDisposable = mLyricSearcher.setSong(mSong) -// .getLyricObservable() -// .doOnSubscribe(disposable -> mStatus = Status.SEARCHING) -// .subscribe(lrcRows -> { -// if (id == mSong.getId()) { -// mStatus = Status.NORMAL; -// mOffset = SPUtil.getValue(App.getContext(), SPUtil.LYRIC_OFFSET_KEY.NAME, id + "", 0); -// mLrcRows = lrcRows; -// } -// }, throwable -> { -// Timber.tag(TAG).v(throwable); -// if (id == mSong.getId()) { -// mStatus = Status.ERROR; -// mLrcRows = null; -// } -// }); -// } -// -// public void setSongAndGetLyricRows(Song song) { -// mSong = song; -// updateLrcRows(); -// } -// -// public Status getStatus() { -// return mStatus; -// } -// -// public void quitImmediately() { -// interrupt(); -// } -// -// @Override -// public void interrupt() { -// super.interrupt(); -// Timber.tag(TAG).v("interrupt"); -// if (mDisposable != null && !mDisposable.isDisposed()) { -// mDisposable.dispose(); -// } -// mReference = null; -// } -// -// protected LyricRowWrapper findCurrentLyric() { -// final MusicService service = mReference != null ? mReference.get() : null; -// if (service == null) { -// return null; -// } -// LyricRowWrapper wrapper = new LyricRowWrapper(); -// wrapper.setStatus(mStatus); -// -// if (mStatus == Status.SEARCHING) { -// return wrapper; -// } -// -// if (mStatus == Status.ERROR || mStatus == Status.NO) { -// Timber.tag(TAG).v("当前没有歌词"); -// return wrapper; -// } -// final Song song = service.getCurrentSong(); -// final int progress = service.getProgress() + mOffset; -// if (mLrcRows == null || mLrcRows.isEmpty()) { -// return wrapper; -// } -// for (int i = mLrcRows.size() - 1; i >= 0; i--) { -// LrcRow lrcRow = mLrcRows.get(i); -// int interval = progress - lrcRow.getTime(); -// if (i == 0 && interval < 0) { -// //未开始歌唱前显示歌曲信息 -// wrapper.setLineOne(new LrcRow("", 0, song.getTitle())); -// wrapper.setLineTwo(new LrcRow("", 0, song.getArtist() + " - " + song.getAlbum())); -// return wrapper; -// } else if (progress >= lrcRow.getTime()) { -// if (lrcRow.hasTranslate()) { -// wrapper.setLineOne(new LrcRow(lrcRow)); -// wrapper.getLineOne().setContent(lrcRow.getContent()); -// wrapper.setLineTwo(new LrcRow(lrcRow)); -// wrapper.getLineTwo().setContent(lrcRow.getTranslate()); -// } else { -// wrapper.setLineOne(lrcRow); -// wrapper.setLineTwo(new LrcRow(i + 1 < mLrcRows.size() ? mLrcRows.get(i + 1) : LYRIC_EMPTY_ROW)); -// } -// return wrapper; -// } -// } -// return wrapper; -// } -// -// -// public enum Status { -// NO, SEARCHING, ERROR, NORMAL -// } -//} diff --git a/app/src/main/java/remix/myplayer/lyric/bean/LrcRow.kt b/app/src/main/java/remix/myplayer/lyric/bean/LrcRow.kt deleted file mode 100644 index 7ebc11db1..000000000 --- a/app/src/main/java/remix/myplayer/lyric/bean/LrcRow.kt +++ /dev/null @@ -1,168 +0,0 @@ -package remix.myplayer.lyric.bean - -import android.text.TextUtils -import com.google.gson.annotations.SerializedName -import remix.myplayer.App -import remix.myplayer.R -import java.util.* - -/** - * 每行歌词的实体类,实现了Comparable接口,方便List的sort排序 - * - * @author Ligang 2014/8/19 - */ -class LrcRow : Comparable { - @SerializedName("mTimeStr") - var timeStr: String = "" - - /** - * 开始时间 毫秒数 00:10:00 为10000 - */ - @SerializedName("mTime") - var time = 0 - - /** - * 歌词内容 - */ - @SerializedName("mContent") - var content: String = "" - - /** - * 歌词内容翻译 - */ - @SerializedName("mTranslate") - var translate: String = "" - - /** - * 该行歌词显示的总时间 - */ - @SerializedName("mTotalTime") - var totalTime: Long = 0 - private set - - /** - * 该行歌词内容所占的高度 - */ - @SerializedName("mContentHeight") - var contentHeight = 0 - - /** - * 该行歌词翻译所占的高度 - */ - @SerializedName("mTranslateHeight") - var translateHeight = 0 - - /** - * 该句歌词所占的总共高度 - */ - @SerializedName("mTotalHeight") - var totalHeight = 0 - - fun setTotalTime(totalTime: Int) { - this.totalTime = totalTime.toLong() - } - - fun hasTranslate(): Boolean { - return !TextUtils.isEmpty(translate) - } - - constructor() {} - constructor(lrcRow: LrcRow) { - timeStr = lrcRow.timeStr - time = lrcRow.time - totalTime = lrcRow.totalTime - content = lrcRow.content - translate = lrcRow.translate - } - - constructor(timeStr: String, time: Int, content: String) : super() { - this.timeStr = timeStr - this.time = time - if (TextUtils.isEmpty(content)) { - this.content = "" - translate = "" - return - } - val mulitiContent = content.split("\t".toRegex()).toTypedArray() - this.content = mulitiContent[0] - if (mulitiContent.size > 1) { - translate = mulitiContent[1] - } - } - - override fun compareTo(row: LrcRow): Int { - return time - row.time - } - - // @Override - // public String toString() { - // return "LrcRow [mTimeStr=" + mTimeStr + ", mTime=" + mTime + ", mTotalTime=" + mTotalTime +", mContent=" - // + mContent + "]"; - // } - override fun toString(): String { - return "[$timeStr] $content" - } - - companion object { - /** - * 将歌词文件中的某一行 解析成一个List 因为一行中可能包含了多个LrcRow对象 比如 [03:33.02][00:36.37]当鸽子不再象征和平 ,就包含了2个对象 - */ - fun createRows(lrcLine: String, offset: Int): List? { - if (!lrcLine.startsWith("[") || !lrcLine.contains("]")) { - return null - } - //最后一个"]" - val lastIndexOfRightBracket = lrcLine.lastIndexOf("]") - //歌词内容 - val content = lrcLine.substring(lastIndexOfRightBracket + 1, lrcLine.length) - //截取出歌词时间,并将"[" 和"]" 替换为"-" [offset:0] - - // -03:33.02--00:36.37- - val times = lrcLine.substring(0, lastIndexOfRightBracket + 1).replace("[", "-") - .replace("]", "-") - val timesArray = times.split("-".toRegex()).toTypedArray() - val lrcRows: MutableList = ArrayList() - for (tem in timesArray) { - //保留空白行 - if (TextUtils.isEmpty(tem.trim { it <= ' ' }) - /**|| TextUtils.isEmpty(content) */ - ) { - continue - } - try { - val lrcRow = LrcRow(tem, formatTime(tem) - offset, content) - lrcRows.add(lrcRow) - } catch (e: Exception) { - } - } - return lrcRows - } - - /**** - * 把歌词时间转换为毫秒值 如 将00:10.00 转为10000 - * @param str - * @return - */ - private fun formatTime(str: String): Int { - val timeStr = str.replace('.', ':') - val times = timeStr.split(":".toRegex()).toTypedArray() - return if (times.size > 2) { - times[0].toInt() * 60 * 1000 + times[1].toInt() * 1000 + times[2].toInt() - } else times[0].toInt() * 60 * 1000 + times[1].toInt() * 1000 - } - - val offset: Int - get() = 0 - - @JvmField - var LYRIC_EMPTY_ROW = LrcRow("", 0, "") - - @JvmField - var LYRIC_NO_ROW = LrcRow("", 0, - App.context.getString(R.string.no_lrc)) - - @JvmField - var LYRIC_SEARCHING_ROW = LrcRow("", 0, - App.context.getString(R.string.searching)) - } -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyric/bean/LyricRowWrapper.kt b/app/src/main/java/remix/myplayer/lyric/bean/LyricRowWrapper.kt deleted file mode 100644 index f8cd67a9a..000000000 --- a/app/src/main/java/remix/myplayer/lyric/bean/LyricRowWrapper.kt +++ /dev/null @@ -1,25 +0,0 @@ -package remix.myplayer.lyric.bean - -import remix.myplayer.lyric.LyricFetcher - -/** - * @ClassName - * @Description - * @Author Xiaoborui - * @Date 2017/5/10 13:37 - */ -data class LyricRowWrapper(var lineOne: LrcRow = LrcRow.LYRIC_EMPTY_ROW, - var lineTwo: LrcRow = LrcRow.LYRIC_EMPTY_ROW, - var status: LyricFetcher.Status = LyricFetcher.Status.NO) { - override fun toString(): String { - return "LyricRowWrapper{" + - "LineOne=" + lineOne + - ", LineTwo=" + lineTwo + - '}' - } - - companion object { - val LYRIC_WRAPPER_NO = LyricRowWrapper(LrcRow.LYRIC_NO_ROW, LrcRow.LYRIC_EMPTY_ROW) - val LYRIC_WRAPPER_SEARCHING = LyricRowWrapper(LrcRow.LYRIC_SEARCHING_ROW, LrcRow.LYRIC_EMPTY_ROW) - } -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyrics/CurrentNextLyricsLine.kt b/app/src/main/java/remix/myplayer/lyrics/CurrentNextLyricsLine.kt new file mode 100644 index 000000000..e9f9fe69b --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/CurrentNextLyricsLine.kt @@ -0,0 +1,11 @@ +package remix.myplayer.lyrics + +data class CurrentNextLyricsLine( + val currentLine: LyricsLine?, + val currentLineProgress: Double?, + val nextLine: LyricsLine?, +) { + companion object { + val SEARCHING = CurrentNextLyricsLine(LyricsLine.LYRICS_LINE_SEARCHING, null, null) + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/LrcParser.kt b/app/src/main/java/remix/myplayer/lyrics/LrcParser.kt new file mode 100644 index 000000000..474fa8ff8 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/LrcParser.kt @@ -0,0 +1,129 @@ +package remix.myplayer.lyrics + +import timber.log.Timber +import kotlin.math.roundToLong + +object LrcParser { + private const val TAG = "LrcParser" + + private val WORD_TIME_TAG_REGEX = """<(\d+:){1,2}\d+(\.\d*)?>""".toRegex() + private val EMPTY_LINE_WITH_TIME_REGEX = """(\[(\d+:){1,2}\d+(\.\d*)?])*""".toRegex() + + /** + * 一般格式: `mm:ss.xx`, `mm:ss.xxx` + * + * @param offset 正数表示更早,负数表示更晚 + * @return 以毫秒为单位;失败返回 null + */ + private fun parseTime(timeStr: String, offset: Long): Long? { + try { + val parts = timeStr.split(':') + val minutes = when (parts.size) { + 2 -> parts[0].toLong() + 3 -> parts[0].toLong() * 60 + parts[1].toLong() + else -> throw Exception("Unknown time format") + } + val seconds = minutes * 60 + parts.last().toDouble() + return (seconds * 1000).roundToLong() - offset + } catch (t: Throwable) { + Timber.tag(TAG).w(t, "Failed to parse time: $timeStr") + } + return null + } + + /** + * 解析精确到字的歌词 + * + * @param time 整行的开始时间 + */ + private fun parseWords(time: Long, offset: Long, content: String): LyricsLine { + val words = ArrayList() + var currentTime = time + var lastStart = 0 + var match = WORD_TIME_TAG_REGEX.find(content) ?: return SimpleLyricsLine(time, content) + while (true) { + words.add(Word(currentTime, content.substring(lastStart, match.range.first))) + parseTime(match.value.substring(1, match.value.lastIndex), offset)?.let { + // 确保同一 LyricsLine 内 time 单调不减 + if (it > currentTime) { + currentTime = it + } + } + lastStart = match.range.last + 1 + match = match.next() ?: break + } + words.add(Word(currentTime, content.substring(lastStart))) + return PerWordLyricsLine(time, words) + } + + fun parse(data: String): ArrayList { + val lines = ArrayList() + var offset = 0L + + data.lines().forEach { + if (it.isBlank()) { + return@forEach + } + if (!it.startsWith('[')) { + Timber.tag(TAG).w("Ignored unknown line: $it") + return@forEach + } + + // [xxx] + // 特判空行 + if (it.endsWith(']') && !EMPTY_LINE_WITH_TIME_REGEX.matches(it)) { + val tag = it.substring(1, it.lastIndex) + // [offset:+/-xxx] + if (tag.startsWith("offset:")) { + try { + offset = tag.substring(7).trim().toLong() + } catch (t: Throwable) { + Timber.tag(TAG).w("Failed to parse offset, raw tag: $tag") + } + return@forEach + } + Timber.tag(TAG).v("Ignored unknown tag: $tag") + return@forEach + } + + var index = 0 + + // 解析时间戳 + val times = ArrayList() + while (it.startsWith("[", index)) { + val closing = it.indexOf(']', index) + if (closing == -1) break + parseTime(it.substring(index + 1, closing), offset)?.let { time -> + times.add(time) + } + index = closing + 1 + } + + if (times.size == 1) { + lines.add(parseWords(times[0], offset, it.substring(index))) + } else { + times.forEach { time -> + lines.add(SimpleLyricsLine(time, it.substring(index))) + } + } + } + + // 合并翻译 + // 相同时间戳的两行,认为第二行是第一行的翻译 + lines.sortBy { it.time } + val combinedLines = ArrayList() + for (line in lines) { + val lastLine = combinedLines.lastOrNull() + combinedLines.add( + if (lastLine?.time == line.time && lastLine.translation == null) { + combinedLines.removeAt(combinedLines.lastIndex) + lastLine.withTranslation(line.content) + } else { + line + } + ) + } + + return combinedLines + } +} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyrics/LyricsLine.kt b/app/src/main/java/remix/myplayer/lyrics/LyricsLine.kt new file mode 100644 index 000000000..c45da1a59 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/LyricsLine.kt @@ -0,0 +1,31 @@ +package remix.myplayer.lyrics + +import kotlinx.serialization.Serializable +import remix.myplayer.App +import remix.myplayer.R + +@Serializable +sealed class LyricsLine { + /** + * 这行歌词的开始时间 + */ + abstract val time: Long + + /** + * 整行歌词的内容,仅文本 + */ + abstract val content: String + + abstract val translation: String? + + abstract fun withTranslation(newTranslation: String?): LyricsLine + + companion object { + val LYRICS_LINE_SEARCHING by lazy { + SimpleLyricsLine(0, App.context.getString(R.string.searching)) + } + val LYRICS_LINE_NO_LRC by lazy { + SimpleLyricsLine(0, App.context.getString(R.string.no_lrc)) + } + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/LyricsManager.kt b/app/src/main/java/remix/myplayer/lyrics/LyricsManager.kt new file mode 100644 index 000000000..f0eb2054d --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/LyricsManager.kt @@ -0,0 +1,335 @@ +package remix.myplayer.lyrics + +import android.app.Activity +import android.content.Context +import android.graphics.PixelFormat +import android.os.Build +import android.view.ContextThemeWrapper +import android.view.Gravity +import android.view.ViewGroup +import android.view.WindowManager +import androidx.annotation.UiThread +import com.hjq.permissions.Permission +import com.hjq.permissions.XXPermissions +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import remix.myplayer.App +import remix.myplayer.R +import remix.myplayer.bean.mp3.Song +import remix.myplayer.helper.MusicServiceRemote +import remix.myplayer.lyrics.provider.ILyricsProvider +import remix.myplayer.theme.ThemeStore +import remix.myplayer.ui.activity.LockScreenActivity +import remix.myplayer.ui.fragment.LyricsFragment +import remix.myplayer.ui.widget.desktop.DesktopLyricsView +import remix.myplayer.util.SPUtil +import remix.myplayer.util.SPUtil.DESKTOP_LYRICS_KEY +import remix.myplayer.util.SPUtil.LYRICS_KEY +import remix.myplayer.util.ToastUtil +import timber.log.Timber +import java.lang.ref.WeakReference +import kotlin.time.Duration.Companion.milliseconds + +object LyricsManager : CoroutineScope by CoroutineScope(Dispatchers.IO) { + private const val TAG = "LyricsManager" + private val UPDATE_INTERVAL = 50.milliseconds + + private val windowManager by lazy { + App.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + } + + private var desktopLyricsView: DesktopLyricsView? = null + + private var lyricsFragment: WeakReference? = null + private var lockScreenActivity: WeakReference? = null + + fun setLyricsFragment(fragment: LyricsFragment) { + lyricsFragment = WeakReference(fragment) + lyrics?.let { + fragment.setLyrics(it) + } ?: fragment.setLyricsSearching() + fragment.setProgress(progress, duration) + fragment.setOffset(offset) + } + + fun setLockScreenActivity(activity: LockScreenActivity) { + lockScreenActivity = WeakReference(activity) + currentNextLyricsLine = currentNextLyricsLine + } + + var isServiceAvailable: Boolean = false + @UiThread set(value) { + field = value + ensureDesktopLyrics() + } + var isNotifyShowing: Boolean = false + @UiThread set(value) { + field = value + ensureDesktopLyrics() + } + var isScreenOn: Boolean = true + @UiThread set(value) { + field = value + ensureDesktopLyrics() + } + var isAppInForeground: Boolean = false + @UiThread set(value) { + field = value + ensureDesktopLyrics() + } + + var isDesktopLyricsEnabled: Boolean + get() = SPUtil.getValue(App.context, LYRICS_KEY.NAME, LYRICS_KEY.DESKTOP_LYRICS_ENABLED, false) + @UiThread private set(value) { + SPUtil.putValue( + App.context, LYRICS_KEY.NAME, LYRICS_KEY.DESKTOP_LYRICS_ENABLED, value + ) + ToastUtil.show( + App.context, if (value) R.string.opened_desktop_lrc else R.string.closed_desktop_lrc + ) + ensureDesktopLyrics() + } + + // 请求权限要 context,setter 没法多传参所以单独出来 + // activity 为 null 表示不在 Activity 里,不请求权限只 toast + @UiThread + fun setDesktopLyricsEnabled(enabled: Boolean, activity: Activity? = null) { + if (enabled && !XXPermissions.isGranted(App.context, Permission.SYSTEM_ALERT_WINDOW)) { + if (activity != null) { + XXPermissions.with(activity) + .permission(Permission.SYSTEM_ALERT_WINDOW) + .request { _, allGranted -> + if (allGranted) { + isDesktopLyricsLocked = true + } + } + } + ToastUtil.show(App.context, R.string.plz_give_float_permission) + return + } + isDesktopLyricsEnabled = enabled + } + + var isStatusBarLyricsEnabled: Boolean + get() = SPUtil.getValue( + App.context, LYRICS_KEY.NAME, LYRICS_KEY.STATUS_BAR_LYRICS_ENABLED, false + ) + set(value) { + SPUtil.putValue( + App.context, LYRICS_KEY.NAME, LYRICS_KEY.STATUS_BAR_LYRICS_ENABLED, value + ) + // TODO: remove existing (but how?) + } + + @UiThread + private fun ensureDesktopLyrics() { + val shouldShow = + isServiceAvailable && isNotifyShowing && isScreenOn && !isAppInForeground && isDesktopLyricsEnabled + if (shouldShow != (desktopLyricsView != null)) { + if (shouldShow) { + createDesktopLyrics() + } else { + removeDesktopLyrics() + } + } + } + + var isDesktopLyricsLocked: Boolean + get() = desktopLyricsView?.isLocked ?: false + @UiThread set(value) { + desktopLyricsView?.run { + isLocked = value + } ?: { + // 没有桌面歌词时自己动设置 + SPUtil.putValue( + App.context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.LOCKED, value + ) + } + } + + @UiThread + private fun createDesktopLyrics() { + check(desktopLyricsView == null) + + if (!XXPermissions.isGranted(App.context, Permission.SYSTEM_ALERT_WINDOW)) { + Timber.tag(TAG).w("No floating window permission, do not create") + return + } + + Timber.tag(TAG).v("Creating desktop lyrics") + + val param = WindowManager.LayoutParams().apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + type = WindowManager.LayoutParams.TYPE_PHONE + } + format = PixelFormat.RGBA_8888 + gravity = Gravity.TOP + width = ViewGroup.LayoutParams.MATCH_PARENT + height = ViewGroup.LayoutParams.WRAP_CONTENT + x = 0 + y = 0 + flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } + + desktopLyricsView = DesktopLyricsView(ContextThemeWrapper(App.context, ThemeStore.themeRes)) + windowManager.addView(desktopLyricsView, param) + desktopLyricsView!!.restoreWindowPosition() + desktopLyricsView!!.isPlaying = isPlaying + desktopLyricsView!!.setLyrics(currentNextLyricsLine) + } + + @UiThread + private fun removeDesktopLyrics() { + Timber.v("Removing desktop lyrics") + check(desktopLyricsView != null) + windowManager.removeView(desktopLyricsView) + desktopLyricsView = null + } + + private fun getProgressOfLine(line: LyricsLine, time: Long, endTime: Long): Double { + require(time in line.time..endTime) + return if (line is PerWordLyricsLine) { + line.getProgress(time, endTime) + } else { + (time - line.time).toDouble() / (endTime - line.time) + } + } + + private fun getCurrentNextLine( + lyrics: List, offset: Long, progress: Long, duration: Long + ): CurrentNextLyricsLine { + if (lyrics.isEmpty()) { + return CurrentNextLyricsLine(LyricsLine.LYRICS_LINE_NO_LRC, null, null) + } + val progressWithOffset = progress + offset + val index = lyrics.binarySearchBy(progressWithOffset) { it.time }.let { + if (it < 0) -(it + 1) - 1 else it + } + if (index < 0) { + check(index == -1) + return CurrentNextLyricsLine(null, null, lyrics[0]) + } + check(index < lyrics.size) + val cur = lyrics[index] + val nxt = lyrics.getOrNull(index + 1) + return CurrentNextLyricsLine( + cur, getProgressOfLine(cur, progressWithOffset, nxt?.time ?: (duration + offset)), nxt + ) + } + + private var lyrics: List? = null + set(value) { + field = value + launch(Dispatchers.Main) { + lyricsFragment?.get()?.let { + if (value == null) { + it.setLyricsSearching() + } else { + it.setLyrics(value) + } + } + } + } + + var isPlaying: Boolean = false + @UiThread set(value) { + field = value + desktopLyricsView?.isPlaying = value + if (value) { + launch(Dispatchers.IO) { + updateProgress() + } + } + } + private var progress: Long = 0 + set(value) { + field = value + launch(Dispatchers.Main) { + lyricsFragment?.get()?.setProgress(value, duration) + } + currentNextLyricsLine = getCurrentNextLine(lyrics ?: return, offset, value, duration) + } + var offset: Long = 0 + @UiThread set(value) { + field = value + lyricsFragment?.get()?.setOffset(offset) + launch(Dispatchers.IO) { + updateProgress() + LyricsSearcher.saveOffset(MusicServiceRemote.getCurrentSong(), value) + } + } + private var duration: Long = 0 + + private var currentNextLyricsLine: CurrentNextLyricsLine = CurrentNextLyricsLine.SEARCHING + set(value) { + if (value != field) { + field = value + currentLyricsLine = value.currentLine?.content ?: "" + launch(Dispatchers.Main) { + lockScreenActivity?.get()?.setLyrics(value) + desktopLyricsView?.setLyrics(value) + } + } + } + + // For status bar lyrics + private var currentLyricsLine: String = "" + set(value) { + if (value != field && isStatusBarLyricsEnabled) { + MusicServiceRemote.service?.run { + field = value + updateNotificationWithLrc(value) + } + } + } + + private val updateMutex = Mutex() + private var updateLyricsJob: Job? = null + private var updateProgressJob: Job? = null + + fun updateProgress() { + if (!updateMutex.tryLock()) { + // 还没拿到歌词或者当前有别的线程在更新 + return + } + try { +// Timber.tag(TAG).d("update progress") + updateProgressJob?.cancel() + progress = MusicServiceRemote.getProgress().toLong() + if (isPlaying) { + updateProgressJob = launch(Dispatchers.IO) { + // TODO: should we consider thread create cost? + delay(UPDATE_INTERVAL) + updateProgress() + } + } + } finally { + updateMutex.unlock() + } + } + + fun updateLyrics(song: Song, provider: ILyricsProvider? = null) { + updateLyricsJob?.cancel() + updateLyricsJob = launch(Dispatchers.IO) { + updateMutex.withLock { + lyrics = null + currentNextLyricsLine = CurrentNextLyricsLine.SEARCHING + val s = LyricsSearcher.getLyricsAndOffset(song, provider) + ensureActive() + duration = song.duration + lyrics = s.first + offset = s.second + } + updateProgress() + } + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/LyricsSearcher.kt b/app/src/main/java/remix/myplayer/lyrics/LyricsSearcher.kt new file mode 100644 index 000000000..0113485de --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/LyricsSearcher.kt @@ -0,0 +1,233 @@ +package remix.myplayer.lyrics + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import remix.myplayer.App +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.provider.EmbeddedProvider +import remix.myplayer.lyrics.provider.ILyricsProvider +import remix.myplayer.lyrics.provider.IgnoredProvider +import remix.myplayer.lyrics.provider.StubProvider +import remix.myplayer.util.SPUtil +import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.security.MessageDigest +import kotlin.math.min + +@OptIn(ExperimentalSerializationApi::class) +object LyricsSearcher { + private const val TAG = "LyricsSearcher" + + private const val CACHE_DIRECTORY_NAME = "lyrics" + + // 同时作为默认顺序 + private val PROVIDERS = listOf( + EmbeddedProvider, + IgnoredProvider, + )/* + old: + DEF(0, App.context.getString(R.string.default_lyric_priority)), + IGNORE(1, App.context.getString(R.string.ignore_lrc)), + EMBEDDED(2, App.context.getString(R.string.embedded_lyric)), + LOCAL(3, App.context.getString(R.string.local)), + KUGOU(4, App.context.getString(R.string.kugou)), + NETEASE(5, App.context.getString(R.string.netease)), + QQ(6, App.context.getString(R.string.qq)), + MANUAL(7, App.context.getString(R.string.select_lrc)); + */ + + private val ID_TO_PROVIDER: Map = run { + val map = HashMap() + PROVIDERS.forEach { + map[it.id] = it + } + map + } + + var order: List + get() { + val providers = ArrayList() + try { + Json.decodeFromString>( + SPUtil.getValue( + App.context, SPUtil.LYRICS_KEY.NAME, SPUtil.LYRICS_KEY.ORDER, "" + ) + ).forEach { id -> + ID_TO_PROVIDER[id]?.let { + if (!providers.contains(it)) { + providers.add(it) + } + } + } + } catch (t: Throwable) { + Timber.tag(TAG).i(t, "Failed to get search order from preference") + } + PROVIDERS.forEach { + if (!providers.contains(it)) { + providers.add(it) + } + } + return providers + } + set(value) { + SPUtil.putValue( + App.context, + SPUtil.LYRICS_KEY.NAME, + SPUtil.LYRICS_KEY.ORDER, + Json.encodeToString(value.map { it.id }) + ) + } + + // See isValidFatFilenameChar in frameworks/base/core/java/android/os/FileUtils.java + private fun isValidFilenameChar(c: Char): Boolean { + return !(c.code in 0x00..0x1f || c.code == 0x7f || listOf( + '"', '*', '/', ':', '<', '>', '?', '\\', '|' + ).contains(c)) + } + + private fun buildValidFilename(name: String, maxLength: Int): String { + val builder = StringBuilder(min(name.length, maxLength)) + for (c in name) { + if (builder.length == maxLength) { + builder.replace(maxLength - 1, maxLength, "~") + break + } + builder.append(if (isValidFilenameChar(c) && !c.isWhitespace() && c != '-') c else '_') + } + return builder.toString() + } + + private fun getStorageKey(song: Song): String { + require(song != Song.EMPTY_SONG) + val rawKey = Json.encodeToString( + listOf( + when (song) { + is Song.Local -> "local" + is Song.Remote -> "remote" + }, + if (song is Song.Local) song.id.toString() else song.data, + song.title, + song.artist, + song.album + ) + ) + val msg = (if (song.album.isNotBlank()) "%1\$s-%2\$s-%3\$s" else "%1\$s-%3\$s").format( + buildValidFilename(song.artist, 8), + buildValidFilename(song.album, 8), + buildValidFilename(song.title, 16) + ) + val digest = MessageDigest.getInstance("SHA-1") + .digest(rawKey.toByteArray()) + .fold("") { str, it -> str + "%02x".format(it) } + return "$msg-$digest" + } + + private fun getCacheFile(storageKey: String, persistent: Boolean): File { + val baseDir: File = App.context.run { + if (persistent) getExternalFilesDir(null) ?: filesDir + else externalCacheDir ?: cacheDir + } + val dir = File(baseDir, CACHE_DIRECTORY_NAME) + dir.mkdirs() + return File(dir, "$storageKey.json") + } + + private fun getCachedOrNull(song: Song): Pair, Long>? { + val key = getStorageKey(song) + listOf(true, false).map { getCacheFile(key, it) }.forEach { + try { + return Pair( + Json.decodeFromStream>(it.inputStream()), SPUtil.getValue( + App.context, SPUtil.LYRICS_KEY.NAME, SPUtil.LYRICS_KEY.OFFSET_PREFIX + key, 0L + ) + ) + } catch (_: FileNotFoundException) { + } catch (t: Throwable) { + Timber.tag(TAG).i(t, "Failed to get lyrics from cache $it") + } + } + return null + } + + private fun clearCache(song: Song) { + val key = getStorageKey(song) + listOf(true, false).map { getCacheFile(key, it) }.forEach { + it.delete() + } + } + + private fun saveLyrics(song: Song, lyrics: List, persistent: Boolean) { + if (song == Song.EMPTY_SONG) { + Timber.tag(TAG).e("Trying to save lyrics for empty song") + return + } + Timber.tag(TAG).v("Saving lyrics to cache, song: $song") + val key = getStorageKey(song) + SPUtil.deleteValue( + App.context, SPUtil.LYRICS_KEY.NAME, SPUtil.LYRICS_KEY.OFFSET_PREFIX + key + ) + if (!persistent) { + getCacheFile(key, true).delete() + } + try { + val cacheFile = getCacheFile(key, persistent) + cacheFile.delete() + cacheFile.createNewFile() + Json.encodeToStream(lyrics, cacheFile.outputStream()) + } catch (t: Throwable) { + Timber.tag(TAG).e(t, "Failed to save lyrics to cache") + } + } + + fun saveOffset(song: Song, offset: Long) { + if (song == Song.EMPTY_SONG) { + Timber.tag(TAG).e("Trying to save offset for empty song") + return + } + Timber.tag(TAG).v("Saving offset: song=$song, offset=$offset") + val key = getStorageKey(song) + SPUtil.putValue( + App.context, SPUtil.LYRICS_KEY.NAME, SPUtil.LYRICS_KEY.OFFSET_PREFIX + key, offset + ) + } + + /** + * @param provider 由用户指定的歌词源(包括恢复默认)或 null + */ + suspend fun getLyricsAndOffset( + song: Song, + provider: ILyricsProvider? + ): Pair, Long> { + if (song == Song.EMPTY_SONG) { + return Pair(listOf(), 0) + } + if (provider == null) { + getCachedOrNull(song)?.let { + Timber.tag(TAG).v("Got lyrics from cache, song: $song") + return it + } + } + Timber.tag(TAG).v("Searching lyrics for song: $song") + (if (provider != StubProvider && provider != null) listOf(provider) else order).forEach { + Timber.tag(TAG).v("Trying provider: ${it.id}") + try { + return Pair(it.getLyrics(song).let { lyrics -> + if (provider != null || it != IgnoredProvider) { + // Fallback 到 ignored 可能是因为网络等问题,如果缓存将会导致以后需要手动点击才能获取到歌词 + saveLyrics(song, lyrics, provider != null && provider !is StubProvider) + } + lyrics + }, 0) + } catch (t: Throwable) { + Timber.tag(TAG).v(t, "Failed to get lyrics from provider `${it.id}`") + } + } + clearCache(song) + Timber.tag(TAG).i("Failed to get lyrics from any provider, returning empty list") + return Pair(listOf(), 0) + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/PerWordLyricsLine.kt b/app/src/main/java/remix/myplayer/lyrics/PerWordLyricsLine.kt new file mode 100644 index 000000000..94bb6b4ed --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/PerWordLyricsLine.kt @@ -0,0 +1,59 @@ +package remix.myplayer.lyrics + +import android.text.SpannedString +import androidx.annotation.ColorInt +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans +import kotlinx.serialization.Serializable +import remix.myplayer.ui.misc.PartialForegroundColorSpan + +@Serializable +data class PerWordLyricsLine( + override val time: Long, val words: List, override val translation: String? = null +) : LyricsLine() { + init { + require(words.isNotEmpty() && time <= words[0].time) + } + + override val content by lazy { + words.joinToString("") { it.content } + } + + override fun withTranslation(newTranslation: String?): PerWordLyricsLine { + return PerWordLyricsLine(time, words, newTranslation) + } + + /** + * @return 0 到 `words.size` 之间的值 + */ + fun getProgress(time: Long, endTime: Long): Double { + require(time >= this.time && time <= endTime) + var index = words.binarySearchBy(time) { it.time } + // TODO: check + if (index >= 0) { + return index.toDouble() + } + index = -(index + 1) + check(index >= 0 && index <= words.size) + return if (index == 0) { + 0.0 + } else { + index - 1 + (time - words[index - 1].time).toDouble() / ((if (index == words.size) endTime else words[index].time) - words[index - 1].time) + } + } + + /** + * @param progress 0 到 `words.size` 之间的值,可通过 `getProgress` 获得 + * @param color 高亮部分的颜色 + */ + fun getSpannedString(progress: Double, @ColorInt color: Int): SpannedString { + require(progress >= 0 && progress <= words.size) + return buildSpannedString { + words.forEachIndexed { index, word -> + inSpans(PartialForegroundColorSpan((progress - index).coerceIn(0.0, 1.0), color)) { + append(word.content) + } + } + } + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/SimpleLyricsLine.kt b/app/src/main/java/remix/myplayer/lyrics/SimpleLyricsLine.kt new file mode 100644 index 000000000..aedc4ac49 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/SimpleLyricsLine.kt @@ -0,0 +1,14 @@ +package remix.myplayer.lyrics + +import kotlinx.serialization.Serializable + +@Serializable +data class SimpleLyricsLine( + override val time: Long, + override val content: String, + override val translation: String? = null +) : LyricsLine() { + override fun withTranslation(newTranslation: String?): SimpleLyricsLine { + return SimpleLyricsLine(time, content, newTranslation) + } +} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyrics/Word.kt b/app/src/main/java/remix/myplayer/lyrics/Word.kt new file mode 100644 index 000000000..11f303495 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/Word.kt @@ -0,0 +1,9 @@ +package remix.myplayer.lyrics + +import kotlinx.serialization.Serializable + +@Serializable +data class Word( + val time: Long, // in ms + val content: String +) \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyrics/provider/EmbeddedProvider.kt b/app/src/main/java/remix/myplayer/lyrics/provider/EmbeddedProvider.kt new file mode 100644 index 000000000..2047d0cd3 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/provider/EmbeddedProvider.kt @@ -0,0 +1,28 @@ +package remix.myplayer.lyrics.provider + +import org.jaudiotagger.audio.AudioFileIO +import org.jaudiotagger.tag.FieldKey +import remix.myplayer.App +import remix.myplayer.R +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.LrcParser +import remix.myplayer.lyrics.LyricsLine +import java.io.File + +object EmbeddedProvider : ILyricsProvider { + override val id = "embedded" + override val displayName by lazy { + App.context.getString(R.string.embedded_lyric) + } + + override suspend fun getLyrics(song: Song): List { + if (song is Song.Local) { + val lrc = AudioFileIO.read(File(song.data)).tag.getFirst(FieldKey.LYRICS) + if (lrc.isNullOrEmpty()) { + throw Exception("Field `LYRICS` doesn't exist or is empty") + } + return LrcParser.parse(lrc) + } + TODO("Reading embedded lyrics of song type ${song.javaClass.simpleName} is not supported yet") + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/provider/ILyricsProvider.kt b/app/src/main/java/remix/myplayer/lyrics/provider/ILyricsProvider.kt new file mode 100644 index 000000000..9647c4de9 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/provider/ILyricsProvider.kt @@ -0,0 +1,16 @@ +package remix.myplayer.lyrics.provider + +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.LyricsLine + +interface ILyricsProvider { + val id: String + val displayName: String + + /** + * 返回的 List 为空不视为失败,仅抛出异常视为失败 + * + * @throws Throwable + */ + suspend fun getLyrics(song: Song): List +} diff --git a/app/src/main/java/remix/myplayer/lyrics/provider/IgnoredProvider.kt b/app/src/main/java/remix/myplayer/lyrics/provider/IgnoredProvider.kt new file mode 100644 index 000000000..818847bd5 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/provider/IgnoredProvider.kt @@ -0,0 +1,17 @@ +package remix.myplayer.lyrics.provider + +import remix.myplayer.App +import remix.myplayer.R +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.LyricsLine + +object IgnoredProvider : ILyricsProvider { + override val id = "ignored" + override val displayName by lazy { + App.context.getString(R.string.ignore_lrc) + } + + override suspend fun getLyrics(song: Song): List { + return listOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/lyrics/provider/StubProvider.kt b/app/src/main/java/remix/myplayer/lyrics/provider/StubProvider.kt new file mode 100644 index 000000000..9ccde35a5 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/provider/StubProvider.kt @@ -0,0 +1,18 @@ +package remix.myplayer.lyrics.provider + +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.LyricsLine + +/** + * 用于恢复默认 + */ +object StubProvider : ILyricsProvider { + override val id: String + get() = throw RuntimeException() + override val displayName: String + get() = throw RuntimeException() + + override suspend fun getLyrics(song: Song): List { + throw RuntimeException() + } +} diff --git a/app/src/main/java/remix/myplayer/lyrics/provider/UriProvider.kt b/app/src/main/java/remix/myplayer/lyrics/provider/UriProvider.kt new file mode 100644 index 000000000..848ddf3c1 --- /dev/null +++ b/app/src/main/java/remix/myplayer/lyrics/provider/UriProvider.kt @@ -0,0 +1,35 @@ +package remix.myplayer.lyrics.provider + +import android.net.Uri +import remix.myplayer.App +import remix.myplayer.bean.mp3.Song +import remix.myplayer.lyrics.LrcParser +import remix.myplayer.lyrics.LyricsLine +import timber.log.Timber + +class UriProvider(private val uri: Uri) : ILyricsProvider { + companion object { + private const val TAG = "UriProvider" + } + + override val id: String = "uri" + override val displayName: String + get() = throw RuntimeException() // 不应该用到 + + override suspend fun getLyrics(song: Song): List { + return try { + App.context.contentResolver.openInputStream(uri)!!.run { + try { + LrcParser.parse(readBytes().decodeToString()) + } catch (t: Throwable) { + throw t + } finally { + close() + } + } + } catch (t: Throwable) { + Timber.tag(TAG).w(t, "Failed to get lyrics from URI: $uri") + emptyList() + } + } +} diff --git a/app/src/main/java/remix/myplayer/misc/cache/DiskCache.java b/app/src/main/java/remix/myplayer/misc/cache/DiskCache.java index 88636d4d8..0714a8a92 100644 --- a/app/src/main/java/remix/myplayer/misc/cache/DiskCache.java +++ b/app/src/main/java/remix/myplayer/misc/cache/DiskCache.java @@ -1,39 +1,15 @@ package remix.myplayer.misc.cache; -import static remix.myplayer.util.Constants.MB; - import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.os.Environment; import android.text.TextUtils; import java.io.File; -import timber.log.Timber; - /** * Created by Remix on 2016/6/14. */ public class DiskCache { - private static DiskLruCache mLrcCache; - - public static void init(Context context, String name) { - try { - File lrcCacheDir = getDiskCacheDir(context, name); - if (!lrcCacheDir.exists()) { - lrcCacheDir.mkdir(); - } - mLrcCache = DiskLruCache.open(lrcCacheDir, getAppVersion(context), 1, 200 * MB); - } catch (Exception e) { - Timber.e(e); - } - } - - public static DiskLruCache getLrcDiskCache() { - return mLrcCache; - } - public static File getDiskCacheDir(Context context, String uniqueName) { String cachePath = ""; if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) @@ -59,14 +35,4 @@ public static File getDiskCacheDir(Context context, String uniqueName) { return new File(cachePath + File.separator + uniqueName); } - public static int getAppVersion(Context context) { - try { - PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - return info.versionCode; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return 1; - } - } diff --git a/app/src/main/java/remix/myplayer/misc/cache/DiskLruCache.java b/app/src/main/java/remix/myplayer/misc/cache/DiskLruCache.java deleted file mode 100644 index d12603bcd..000000000 --- a/app/src/main/java/remix/myplayer/misc/cache/DiskLruCache.java +++ /dev/null @@ -1,947 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package remix.myplayer.misc.cache; - -import java.io.BufferedInputStream; -import java.io.BufferedWriter; -import java.io.Closeable; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Array; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import timber.log.Timber; - -/** - * ***************************************************************************** Taken from the JB source code, can be - * found in: libcore/luni/src/main/java/libcore/io/DiskLruCache.java or direct link: - * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java - * ***************************************************************************** - *

- * A cache that uses a bounded amount of space on a filesystem. Each cache entry has a string key and a fixed number of - * values. Values are byte sequences, accessible as streams or files. Each value must be between {@code 0} and {@code - * Integer.MAX_VALUE} bytes in length. - * - *

The cache stores its data in a directory on the filesystem. This - * directory must be exclusive to the cache; the cache may deleteSongs or overwrite files from its directory. It is an - * error for multiple processes to use the same cache directory at the same time. - * - *

This cache limits the number of bytes that it will store on the - * filesystem. When the number of stored bytes exceeds the limit, the cache will remove entries in the background until - * the limit is satisfied. The limit is not strict: the cache may temporarily exceed it while waiting for files to be - * deleted. The limit does not include filesystem overhead or the cache journal so space-sensitive applications should - * set a conservative limit. - * - *

Clients call {@link #edit} to create or update the values of an entry. An - * entry may have only one editor at one time; if a value is not available to be edited then {@link #edit} will return - * null. - *

    - *
  • When an entry is being created it is necessary to - * supply a full set of values; the empty value should be used as a placeholder if necessary. - *
  • When an entry is being edited, it is not necessary - * to supply data for every value; values default to their previous value. - *
- * Every {@link #edit} call must be matched by a call to {@link Editor#commit} or {@link Editor#abort}. Committing is - * atomic: a read observes the full set of values as they were before or after the commit, but never a mix of values. - * - *

Clients call {@link #get} to read a snapshot of an entry. The read will - * observe the value at the time that {@link #get} was called. Updates and removals after the call do not impact ongoing - * reads. - * - *

This class is tolerant of some I/O errors. If files are missing from the - * filesystem, the corresponding entries will be dropped from the cache. If an error occurs while writing a cache value, - * the edit will fail silently. Callers should handle other problems by catching {@code IOException} and responding - * appropriately. - */ -public final class DiskLruCache implements Closeable { - - static final String JOURNAL_FILE = "journal"; - static final String JOURNAL_FILE_TMP = "journal.tmp"; - static final String MAGIC = "libcore.io.DiskLruCache"; - static final String VERSION_1 = "1"; - static final long ANY_SEQUENCE_NUMBER = -1; - private static final String CLEAN = "CLEAN"; - private static final String DIRTY = "DIRTY"; - private static final String REMOVE = "REMOVE"; - private static final String READ = "READ"; - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - private static final int IO_BUFFER_SIZE = 8 * 1024; - - /* - * This cache uses a journal file named "journal". A typical journal file - * looks like this: - * libcore.io.DiskLruCache - * 1 - * 100 - * 2 - * - * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 - * DIRTY 335c4c6028171cfddfbaae1a9c313c52 - * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 - * REMOVE 335c4c6028171cfddfbaae1a9c313c52 - * DIRTY 1ab96a171faeeee38496d8b330771a7a - * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 - * READ 335c4c6028171cfddfbaae1a9c313c52 - * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 - * - * The first five lines of the journal form its header. They are the - * constant string "libcore.io.DiskLruCache", the disk cache's version, - * the application's version, the value count, and a blank line. - * - * Each of the subsequent lines in the file is a record of the state of a - * cache entry. Each line contains space-separated values: a state, a key, - * and optional state-specific values. - * o DIRTY lines track that an entry is actively being created or updated. - * Every successful DIRTY action should be followed by a CLEAN or REMOVE - * action. DIRTY lines without a matching CLEAN or REMOVE indicate that - * temporary files may need to be deleted. - * o CLEAN lines track a cache entry that has been successfully published - * and may be read. A publish line is followed by the lengths of each of - * its values. - * o READ lines track accesses for LRU. - * o REMOVE lines track entries that have been deleted. - * - * The journal file is appended to as cache operations occur. The journal may - * occasionally be compacted by dropping redundant lines. A temporary file named - * "journal.tmp" will be used during compaction; that file should be deleted if - * it exists when the cache is opened. - */ - - private final File directory; - private final File journalFile; - private final File journalFileTmp; - private final int appVersion; - private final long maxSize; - private final int valueCount; - private long size = 0; - private Writer journalWriter; - private final LinkedHashMap lruEntries - = new LinkedHashMap(0, 0.75f, true); - private int redundantOpCount; - - /** - * To differentiate between old and current snapshots, each entry is given a sequence number each time an edit is - * committed. A snapshot is stale if its sequence number is not equal to its entry's sequence number. - */ - private long nextSequenceNumber = 0; - - /* From java.util.Arrays */ - @SuppressWarnings("unchecked") - private static T[] copyOfRange(T[] original, int start, int end) { - final int originalLength = original.length; // For exception priority compatibility. - if (start > end) { - throw new IllegalArgumentException(); - } - if (start < 0 || start > originalLength) { - throw new ArrayIndexOutOfBoundsException(); - } - final int resultLength = end - start; - final int copyLength = Math.min(resultLength, originalLength - start); - final T[] result = (T[]) Array - .newInstance(original.getClass().getComponentType(), resultLength); - System.arraycopy(original, start, result, 0, copyLength); - return result; - } - - /** - * Returns the remainder of 'reader' as a string, closing it when done. - */ - public static String readFully(Reader reader) throws IOException { - try { - StringWriter writer = new StringWriter(); - char[] buffer = new char[1024]; - int count; - while ((count = reader.read(buffer)) != -1) { - writer.write(buffer, 0, count); - } - return writer.toString(); - } finally { - reader.close(); - } - } - - /** - * Returns the ASCII characters up to but not including the next "\r\n", or "\n". - * - * @throws java.io.EOFException if the stream is exhausted before the next newline character. - */ - public static String readAsciiLine(InputStream in) throws IOException { - // TODO: support UTF-8 here instead - - StringBuilder result = new StringBuilder(80); - while (true) { - int c = in.read(); - if (c == -1) { - throw new EOFException(); - } else if (c == '\n') { - break; - } - - result.append((char) c); - } - int length = result.length(); - if (length > 0 && result.charAt(length - 1) == '\r') { - result.setLength(length - 1); - } - return result.toString(); - } - - /** - * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. - */ - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Recursively deleteSongs everything in {@code dir}. - */ - // TODO: this should specify paths as Strings rather than as Files - public static void deleteContents(File dir) throws IOException { - File[] files = dir.listFiles(); - if (files == null) { - throw new IllegalArgumentException("not a directory: " + dir); - } - for (File file : files) { - if (file.isDirectory()) { - deleteContents(file); - } - if (!file.delete()) { - throw new IOException("failed to deleteSongs file: " + file); - } - } - } - - /** - * This cache uses a single background thread to evict entries. - */ - private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, - 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); - private final Callable cleanupCallable = new Callable() { - @Override - public Void call() throws Exception { - synchronized (DiskLruCache.this) { - if (journalWriter == null) { - return null; // closed - } - trimToSize(); - if (journalRebuildRequired()) { - rebuildJournal(); - redundantOpCount = 0; - } - } - return null; - } - }; - - private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { - this.directory = directory; - this.appVersion = appVersion; - this.journalFile = new File(directory, JOURNAL_FILE); - this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); - this.valueCount = valueCount; - this.maxSize = maxSize; - } - - /** - * Opens the cache in {@code directory}, creating a cache if none exists there. - * - * @param directory a writable directory - * @param valueCount the number of values per cache entry. Must be positive. - * @param maxSize the maximum number of bytes this cache should use to store - * @throws java.io.IOException if reading or writing the cache directory fails - */ - public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) - throws IOException { - if (maxSize <= 0) { - throw new IllegalArgumentException("maxSize <= 0"); - } - if (valueCount <= 0) { - throw new IllegalArgumentException("valueCount <= 0"); - } - - // prefer to pick up where we left off - DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - if (cache.journalFile.exists()) { - try { - cache.readJournal(); - cache.processJournal(); - cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), - IO_BUFFER_SIZE); - return cache; - } catch (IOException journalIsCorrupt) { - Timber.v("DiskLruCache " + directory + " is corrupt: " - + journalIsCorrupt.getMessage() + ", removing"); - cache.delete(); - } - } - - // create a new empty cache - directory.mkdirs(); - cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - cache.rebuildJournal(); - return cache; - } - - private void readJournal() throws IOException { - InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE); - try { - String magic = readAsciiLine(in); - String version = readAsciiLine(in); - String appVersionString = readAsciiLine(in); - String valueCountString = readAsciiLine(in); - String blank = readAsciiLine(in); - if (!MAGIC.equals(magic) - || !VERSION_1.equals(version) - || !Integer.toString(appVersion).equals(appVersionString) - || !Integer.toString(valueCount).equals(valueCountString) - || !"".equals(blank)) { - throw new IOException("unexpected journal header: [" - + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); - } - - while (true) { - try { - readJournalLine(readAsciiLine(in)); - } catch (EOFException endOfJournal) { - break; - } - } - } finally { - closeQuietly(in); - } - } - - private void readJournalLine(String line) throws IOException { - String[] parts = line.split(" "); - if (parts.length < 2) { - throw new IOException("unexpected journal line: " + line); - } - - String key = parts[1]; - if (parts[0].equals(REMOVE) && parts.length == 2) { - lruEntries.remove(key); - return; - } - - Entry entry = lruEntries.get(key); - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } - - if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { - entry.readable = true; - entry.currentEditor = null; - entry.setLengths(copyOfRange(parts, 2, parts.length)); - } else if (parts[0].equals(DIRTY) && parts.length == 2) { - entry.currentEditor = new Editor(entry); - } else if (parts[0].equals(READ) && parts.length == 2) { - // this work was already done by calling lruEntries.get() - } else { - throw new IOException("unexpected journal line: " + line); - } - } - - /** - * Computes the initial size and collects garbage as a part of opening the cache. Dirty entries are assumed to be - * inconsistent and will be deleted. - */ - private void processJournal() throws IOException { - deleteIfExists(journalFileTmp); - for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { - Entry entry = i.next(); - if (entry.currentEditor == null) { - for (int t = 0; t < valueCount; t++) { - size += entry.lengths[t]; - } - } else { - entry.currentEditor = null; - for (int t = 0; t < valueCount; t++) { - deleteIfExists(entry.getCleanFile(t)); - deleteIfExists(entry.getDirtyFile(t)); - } - i.remove(); - } - } - } - - /** - * Creates a new journal that omits redundant information. This replaces the current journal if it exists. - */ - private synchronized void rebuildJournal() throws IOException { - if (journalWriter != null) { - journalWriter.close(); - } - - Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); - writer.write(MAGIC); - writer.write("\n"); - writer.write(VERSION_1); - writer.write("\n"); - writer.write(Integer.toString(appVersion)); - writer.write("\n"); - writer.write(Integer.toString(valueCount)); - writer.write("\n"); - writer.write("\n"); - - for (Entry entry : lruEntries.values()) { - if (entry.currentEditor != null) { - writer.write(DIRTY + ' ' + entry.key + '\n'); - } else { - writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - } - } - - writer.close(); - journalFileTmp.renameTo(journalFile); - journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); - } - - private static void deleteIfExists(File file) throws IOException { -// try { -// Libcore.os.remove(file.getPath()); -// } catch (ErrnoException errnoException) { -// if (errnoException.errno != OsConstants.ENOENT) { -// throw errnoException.rethrowAsIOException(); -// } -// } - if (file.exists() && !file.delete()) { - throw new IOException(); - } - } - - /** - * Returns a snapshot of the entry named {@code key}, or null if it doesn't exist is not currently readable. If a - * value is returned, it is moved to the head of the LRU queue. - */ - public synchronized Snapshot get(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null) { - return null; - } - - if (!entry.readable) { - return null; - } - - /* - * Open all streams eagerly to guarantee that we see a single published - * snapshot. If we opened streams lazily then the streams could come - * from different edits. - */ - InputStream[] ins = new InputStream[valueCount]; - try { - for (int i = 0; i < valueCount; i++) { - ins[i] = new FileInputStream(entry.getCleanFile(i)); - } - } catch (FileNotFoundException e) { - // a file must have been deleted manually! - return null; - } - - redundantOpCount++; - journalWriter.append(READ + ' ' + key + '\n'); - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return new Snapshot(key, entry.sequenceNumber, ins); - } - - /** - * Returns an editor for the entry named {@code key}, or null if another edit is in progress. - */ - public Editor edit(String key) throws IOException { - return edit(key, ANY_SEQUENCE_NUMBER); - } - - private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER - && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { - return null; // snapshot is stale - } - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } else if (entry.currentEditor != null) { - return null; // another edit is in progress - } - - Editor editor = new Editor(entry); - entry.currentEditor = editor; - - // flush the journal before creating files to prevent file leaks - journalWriter.write(DIRTY + ' ' + key + '\n'); - journalWriter.flush(); - return editor; - } - - /** - * Returns the directory where this cache stores its data. - */ - public File getDirectory() { - return directory; - } - - /** - * Returns the maximum number of bytes that this cache should use to store its data. - */ - public long maxSize() { - return maxSize; - } - - /** - * Returns the number of bytes currently being used to store the values in this cache. This may be greater than the - * max size if a background deletion is pending. - */ - public synchronized long size() { - return size; - } - - private synchronized void completeEdit(Editor editor, boolean success) throws IOException { - Entry entry = editor.entry; - if (entry.currentEditor != editor) { - throw new IllegalStateException(); - } - - // if this edit is creating the entry for the first time, every index must have a value - if (success && !entry.readable) { - for (int i = 0; i < valueCount; i++) { - if (!entry.getDirtyFile(i).exists()) { - editor.abort(); - throw new IllegalStateException("edit didn't create file " + i); - } - } - } - - for (int i = 0; i < valueCount; i++) { - File dirty = entry.getDirtyFile(i); - if (success) { - if (dirty.exists()) { - File clean = entry.getCleanFile(i); - dirty.renameTo(clean); - long oldLength = entry.lengths[i]; - long newLength = clean.length(); - entry.lengths[i] = newLength; - size = size - oldLength + newLength; - } - } else { - deleteIfExists(dirty); - } - } - - redundantOpCount++; - entry.currentEditor = null; - if (entry.readable | success) { - entry.readable = true; - journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - if (success) { - entry.sequenceNumber = nextSequenceNumber++; - } - } else { - lruEntries.remove(entry.key); - journalWriter.write(REMOVE + ' ' + entry.key + '\n'); - } - - if (size > maxSize || journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - } - - /** - * We only rebuild the journal when it will halve the size of the journal and eliminate at least 2000 ops. - */ - private boolean journalRebuildRequired() { - final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; - return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD - && redundantOpCount >= lruEntries.size(); - } - - /** - * Drops the entry for {@code key} if it exists and can be removed. Entries actively being edited cannot be removed. - * - * @return true if an entry was removed. - */ - public synchronized boolean remove(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null || entry.currentEditor != null) { - return false; - } - - for (int i = 0; i < valueCount; i++) { - File file = entry.getCleanFile(i); - if (!file.delete()) { - throw new IOException("failed to delete " + file); - } - size -= entry.lengths[i]; - entry.lengths[i] = 0; - } - - redundantOpCount++; - journalWriter.append(REMOVE + ' ' + key + '\n'); - lruEntries.remove(key); - - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return true; - } - - /** - * Returns true if this cache has been closed. - */ - public boolean isClosed() { - return journalWriter == null; - } - - private void checkNotClosed() { - if (journalWriter == null) { - throw new IllegalStateException("cache is closed"); - } - } - - /** - * Force buffered operations to the filesystem. - */ - public synchronized void flush() throws IOException { - checkNotClosed(); - trimToSize(); - journalWriter.flush(); - } - - /** - * Closes this cache. Stored values will remain on the filesystem. - */ - public synchronized void close() throws IOException { - if (journalWriter == null) { - return; // already closed - } - for (Entry entry : new ArrayList(lruEntries.values())) { - if (entry.currentEditor != null) { - entry.currentEditor.abort(); - } - } - trimToSize(); - journalWriter.close(); - journalWriter = null; - } - - private void trimToSize() throws IOException { - while (size > maxSize) { -// Map.Entry toEvict = lruEntries.eldest(); - final Map.Entry toEvict = lruEntries.entrySet().iterator().next(); - remove(toEvict.getKey()); - } - } - - /** - * Closes the cache and deletes all of its stored values. This will deleteSongs all files in the cache directory - * including files that weren't created by the cache. - */ - public void delete() throws IOException { - close(); - deleteContents(directory); - } - - private void validateKey(String key) { - if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { - throw new IllegalArgumentException( - "keys must not contain spaces or newlines: \"" + key + "\""); - } - } - - private static String inputStreamToString(InputStream in) throws IOException { - return readFully(new InputStreamReader(in, UTF_8)); - } - - /** - * A snapshot of the values for an entry. - */ - public final class Snapshot implements Closeable { - - private final String key; - private final long sequenceNumber; - private final InputStream[] ins; - - private Snapshot(String key, long sequenceNumber, InputStream[] ins) { - this.key = key; - this.sequenceNumber = sequenceNumber; - this.ins = ins; - } - - /** - * Returns an editor for this snapshot's entry, or null if either the entry has changed since this snapshot was - * created or if another edit is in progress. - */ - public Editor edit() throws IOException { - return DiskLruCache.this.edit(key, sequenceNumber); - } - - /** - * Returns the unbuffered stream with the value for {@code index}. - */ - public InputStream getInputStream(int index) { - return ins[index]; - } - - /** - * Returns the string value for {@code index}. - */ - public String getString(int index) throws IOException { - return inputStreamToString(getInputStream(index)); - } - - @Override - public void close() { - for (InputStream in : ins) { - closeQuietly(in); - } - } - } - - /** - * Edits the values for an entry. - */ - public final class Editor { - - private final Entry entry; - private boolean hasErrors; - - private Editor(Entry entry) { - this.entry = entry; - } - - /** - * Returns an unbuffered input stream to read the last committed value, or null if no value has been committed. - */ - public InputStream newInputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - if (!entry.readable) { - return null; - } - return new FileInputStream(entry.getCleanFile(index)); - } - } - - /** - * Returns the last committed value as a string, or null if no value has been committed. - */ - public String getString(int index) throws IOException { - InputStream in = newInputStream(index); - return in != null ? inputStreamToString(in) : null; - } - - /** - * Returns a new unbuffered output stream to write the value at {@code index}. If the underlying output stream - * encounters errors when writing to the filesystem, this edit will be aborted when {@link #commit} is called. The - * returned output stream does not throw IOExceptions. - */ - public OutputStream newOutputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); - } - } - - /** - * Sets the value at {@code index} to {@code value}. - */ - public void set(int index, String value) throws IOException { - Writer writer = null; - try { - writer = new OutputStreamWriter(newOutputStream(index), UTF_8); - writer.write(value); - } finally { - closeQuietly(writer); - } - } - - /** - * Commits this edit so it is visible to readers. This releases the edit lock so another edit may be started on the - * same key. - */ - public void commit() throws IOException { - if (hasErrors) { - completeEdit(this, false); - remove(entry.key); // the previous entry is stale - } else { - completeEdit(this, true); - } - } - - /** - * Aborts this edit. This releases the edit lock so another edit may be started on the same key. - */ - public void abort() throws IOException { - completeEdit(this, false); - } - - private class FaultHidingOutputStream extends FilterOutputStream { - - private FaultHidingOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(int oneByte) { - try { - out.write(oneByte); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void write(byte[] buffer, int offset, int length) { - try { - out.write(buffer, offset, length); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void close() { - try { - out.close(); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void flush() { - try { - out.flush(); - } catch (IOException e) { - hasErrors = true; - } - } - } - } - - private final class Entry { - - private final String key; - - /** - * Lengths of this entry's files. - */ - private final long[] lengths; - - /** - * True if this entry has ever been published - */ - private boolean readable; - - /** - * The ongoing edit or null if this entry is not being edited. - */ - private Editor currentEditor; - - /** - * The sequence number of the most recently committed edit to this entry. - */ - private long sequenceNumber; - - private Entry(String key) { - this.key = key; - this.lengths = new long[valueCount]; - } - - public String getLengths() throws IOException { - StringBuilder result = new StringBuilder(); - for (long size : lengths) { - result.append(' ').append(size); - } - return result.toString(); - } - - /** - * Set lengths using decimal numbers like "10123". - */ - private void setLengths(String[] strings) throws IOException { - if (strings.length != valueCount) { - throw invalidLengths(strings); - } - - try { - for (int i = 0; i < strings.length; i++) { - lengths[i] = Long.parseLong(strings[i]); - } - } catch (NumberFormatException e) { - throw invalidLengths(strings); - } - } - - private IOException invalidLengths(String[] strings) throws IOException { - throw new IOException("unexpected journal line: " + Arrays.toString(strings)); - } - - public File getCleanFile(int i) { - return new File(directory, key + "." + i); - } - - public File getDirtyFile(int i) { - return new File(directory, key + "." + i + ".tmp"); - } - } -} diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/FloatWindowManager.java b/app/src/main/java/remix/myplayer/misc/floatpermission/FloatWindowManager.java deleted file mode 100644 index 188e54d40..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/FloatWindowManager.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.provider.Settings; -import android.util.Log; -import android.view.WindowManager; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import remix.myplayer.misc.floatpermission.rom.HuaweiUtils; -import remix.myplayer.misc.floatpermission.rom.MeizuUtils; -import remix.myplayer.misc.floatpermission.rom.MiuiUtils; -import remix.myplayer.misc.floatpermission.rom.QikuUtils; -import remix.myplayer.misc.floatpermission.rom.RomUtils; - -/** - * Description: - * - * @author zhaozp - * @since 2016-10-17 - */ - -public class FloatWindowManager { - - private static final String TAG = "FloatWindowManager"; - - private static volatile FloatWindowManager instance; - - private boolean isWindowDismiss = true; - private WindowManager windowManager = null; - private WindowManager.LayoutParams mParams = null; - private Dialog dialog; - - public static FloatWindowManager getInstance() { - if (instance == null) { - synchronized (FloatWindowManager.class) { - if (instance == null) { - instance = new FloatWindowManager(); - } - } - } - return instance; - } - - - public boolean checkPermission(Context context) { - //6.0 版本之后由于 google 增加了对悬浮窗权限的管理,所以方式就统一了 - if (Build.VERSION.SDK_INT < 23) { - if (RomUtils.checkIsMiuiRom()) { - return miuiPermissionCheck(context); - } else if (RomUtils.checkIsMeizuRom()) { - return meizuPermissionCheck(context); - } else if (RomUtils.checkIsHuaweiRom()) { - return huaweiPermissionCheck(context); - } else if (RomUtils.checkIs360Rom()) { - return qikuPermissionCheck(context); - } - } - return commonROMPermissionCheck(context); - } - - private boolean huaweiPermissionCheck(Context context) { - return HuaweiUtils.checkFloatWindowPermission(context); - } - - private static boolean miuiPermissionCheck(Context context) { - return MiuiUtils.checkFloatWindowPermission(context); - } - - private boolean meizuPermissionCheck(Context context) { - return MeizuUtils.checkFloatWindowPermission(context); - } - - private boolean qikuPermissionCheck(Context context) { - return QikuUtils.checkFloatWindowPermission(context); - } - - private boolean commonROMPermissionCheck(Context context) { - //最新发现魅族6.0的系统这种方式不好用,天杀的,只有你是奇葩,没办法,单独适配一下 - if (RomUtils.checkIsMeizuRom()) { - return meizuPermissionCheck(context); - } else { - Boolean result = true; - if (Build.VERSION.SDK_INT >= 23) { - try { - Class clazz = Settings.class; - Method canDrawOverlays = clazz.getDeclaredMethod("canDrawOverlays", Context.class); - result = (Boolean) canDrawOverlays.invoke(null, context); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return result; - } - } - - private void applyPermission(Context context) { - if (Build.VERSION.SDK_INT < 23) { - if (RomUtils.checkIsMiuiRom()) { - miuiROMPermissionApply(context); - } else if (RomUtils.checkIsMeizuRom()) { - meizuROMPermissionApply(context); - } else if (RomUtils.checkIsHuaweiRom()) { - huaweiROMPermissionApply(context); - } else if (RomUtils.checkIs360Rom()) { - ROM360PermissionApply(context); - } - } - commonROMPermissionApply(context); - } - - private void ROM360PermissionApply(final Context context) { - showConfirmDialog(context, new OnConfirmResult() { - @Override - public void confirmResult(boolean confirm) { - if (confirm) { - QikuUtils.applyPermission(context); - } else { - Log.e(TAG, "ROM:360, user manually refuse OVERLAY_PERMISSION"); - } - } - }); - } - - private void huaweiROMPermissionApply(final Context context) { - showConfirmDialog(context, new OnConfirmResult() { - @Override - public void confirmResult(boolean confirm) { - if (confirm) { - HuaweiUtils.applyPermission(context); - } else { - Log.e(TAG, "ROM:huawei, user manually refuse OVERLAY_PERMISSION"); - } - } - }); - } - - private void meizuROMPermissionApply(final Context context) { - showConfirmDialog(context, new OnConfirmResult() { - @Override - public void confirmResult(boolean confirm) { - if (confirm) { - MeizuUtils.applyPermission(context); - } else { - Log.e(TAG, "ROM:meizu, user manually refuse OVERLAY_PERMISSION"); - } - } - }); - } - - private void miuiROMPermissionApply(final Context context) { - showConfirmDialog(context, new OnConfirmResult() { - @Override - public void confirmResult(boolean confirm) { - if (confirm) { - MiuiUtils.applyMiuiPermission(context); - } else { - Log.e(TAG, "ROM:miui, user manually refuse OVERLAY_PERMISSION"); - } - } - }); - } - - /** - * 通用 rom 权限申请 - */ - private void commonROMPermissionApply(final Context context) { - //这里也一样,魅族系统需要单独适配 - if (RomUtils.checkIsMeizuRom()) { - meizuROMPermissionApply(context); - } else { - if (Build.VERSION.SDK_INT >= 23) { - showConfirmDialog(context, new OnConfirmResult() { - @Override - public void confirmResult(boolean confirm) { - if (confirm) { - try { - Class clazz = Settings.class; - Field field = clazz.getDeclaredField("ACTION_MANAGE_OVERLAY_PERMISSION"); - - Intent intent = new Intent(field.get(null).toString()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.setData(Uri.parse("package:" + context.getPackageName())); - context.startActivity(intent); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else { - Log.d(TAG, "user manually refuse OVERLAY_PERMISSION"); - //需要做统计效果 - } - } - }); - } - } - } - - private void showConfirmDialog(Context context, OnConfirmResult result) { - showConfirmDialog(context, "您的手机没有授予悬浮窗权限,请开启后再试", result); - } - - private void showConfirmDialog(Context context, String message, final OnConfirmResult result) { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - - dialog = new AlertDialog.Builder(context).setCancelable(true).setTitle("") - .setMessage(message) - .setPositiveButton("现在去开启", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - result.confirmResult(true); - dialog.dismiss(); - } - }).setNegativeButton("暂不开启", - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - result.confirmResult(false); - dialog.dismiss(); - } - }).create(); - dialog.show(); - } - - private interface OnConfirmResult { - - void confirmResult(boolean confirm); - } -} diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/HuaweiUtils.java b/app/src/main/java/remix/myplayer/misc/floatpermission/rom/HuaweiUtils.java deleted file mode 100644 index 9fccc36b3..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/HuaweiUtils.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission.rom; - -import android.annotation.TargetApi; -import android.app.AppOpsManager; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.Build; -import android.util.Log; -import android.widget.Toast; -import java.lang.reflect.Method; - -public class HuaweiUtils { - - private static final String TAG = "HuaweiUtils"; - - /** - * 检测 Huawei 悬浮窗权限 - */ - public static boolean checkFloatWindowPermission(Context context) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; - } - return true; - } - - /** - * 去华为权限申请页面 - */ - public static void applyPermission(Context context) { - try { - Intent intent = new Intent(); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// ComponentName comp = new ComponentName("com.huawei.systemmanager","com.huawei.permissionmanager.ui.MainActivity");//华为权限管理 -// ComponentName comp = new ComponentName("com.huawei.systemmanager", -// "com.huawei.permissionmanager.ui.SingleAppActivity");//华为权限管理,跳转到指定app的权限管理位置需要华为接口权限,未解决 - ComponentName comp = new ComponentName("com.huawei.systemmanager", - "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity");//悬浮窗管理页面 - intent.setComponent(comp); - if (RomUtils.getEmuiVersion() == 3.1) { - //emui 3.1 的适配 - context.startActivity(intent); - } else { - //emui 3.0 的适配 - comp = new ComponentName("com.huawei.systemmanager", - "com.huawei.notificationmanager.ui.NotificationManagmentActivity");//悬浮窗管理页面 - intent.setComponent(comp); - context.startActivity(intent); - } - } catch (SecurityException e) { - Intent intent = new Intent(); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// ComponentName comp = new ComponentName("com.huawei.systemmanager","com.huawei.permissionmanager.ui.MainActivity");//华为权限管理 - ComponentName comp = new ComponentName("com.huawei.systemmanager", - "com.huawei.permissionmanager.ui.MainActivity");//华为权限管理,跳转到本app的权限管理页面,这个需要华为接口权限,未解决 -// ComponentName comp = new ComponentName("com.huawei.systemmanager","com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity");//悬浮窗管理页面 - intent.setComponent(comp); - context.startActivity(intent); - Log.e(TAG, Log.getStackTraceString(e)); - } catch (ActivityNotFoundException e) { - /** - * 手机管家版本较低 HUAWEI SC-UL10 - */ -// Toast.makeText(MainActivity.this, "act找不到", Toast.LENGTH_LONG).show(); - Intent intent = new Intent(); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ComponentName comp = new ComponentName("com.Android.settings", - "com.android.settings.permission.TabItem");//权限管理页面 android4.4 -// ComponentName comp = new ComponentName("com.android.settings","com.android.settings.permission.single_app_activity");//此处可跳转到指定app对应的权限管理页面,但是需要相关权限,未解决 - intent.setComponent(comp); - context.startActivity(intent); - e.printStackTrace(); - Log.e(TAG, Log.getStackTraceString(e)); - } catch (Exception e) { - //抛出异常时提示信息 - Toast.makeText(context, "进入设置页面失败,请手动设置", Toast.LENGTH_LONG).show(); - Log.e(TAG, Log.getStackTraceString(e)); - } - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private static boolean checkOp(Context context, int op) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - try { - Class clazz = AppOpsManager.class; - Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class); - return AppOpsManager.MODE_ALLOWED == (int) method - .invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else { - Log.e(TAG, "Below API 19 cannot invoke!"); - } - return false; - } -} - - diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MeizuUtils.java b/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MeizuUtils.java deleted file mode 100644 index ad3b10061..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MeizuUtils.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission.rom; - -import android.annotation.TargetApi; -import android.app.AppOpsManager; -import android.content.Context; -import android.content.Intent; -import android.os.Binder; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Method; - -public class MeizuUtils { - - private static final String TAG = "MeizuUtils"; - - /** - * 检测 meizu 悬浮窗权限 - */ - public static boolean checkFloatWindowPermission(Context context) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; - } - return true; - } - - /** - * 去魅族权限申请页面 - */ - public static void applyPermission(Context context) { - Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); - intent.setClassName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity"); - intent.putExtra("packageName", context.getPackageName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private static boolean checkOp(Context context, int op) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - try { - Class clazz = AppOpsManager.class; - Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class); - return AppOpsManager.MODE_ALLOWED == (int) method - .invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else { - Log.e(TAG, "Below API 19 cannot invoke!"); - } - return false; - } -} diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MiuiUtils.java b/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MiuiUtils.java deleted file mode 100644 index e7438d371..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/MiuiUtils.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission.rom; - -import android.annotation.TargetApi; -import android.app.AppOpsManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Binder; -import android.os.Build; -import android.provider.Settings; -import android.util.Log; -import java.lang.reflect.Method; - -public class MiuiUtils { - - private static final String TAG = "MiuiUtils"; - - /** - * 获取小米 rom 版本号,获取失败返回 -1 - * - * @return miui rom version code, if fail , return -1 - */ - public static int getMiuiVersion() { - String version = RomUtils.getSystemProperty("ro.miui.ui.version.name"); - if (version != null) { - try { - return Integer.parseInt(version.substring(1)); - } catch (Exception e) { - Log.e(TAG, "get miui version code error, version : " + version); - Log.e(TAG, Log.getStackTraceString(e)); - } - } - return -1; - } - - /** - * 检测 miui 悬浮窗权限 - */ - public static boolean checkFloatWindowPermission(Context context) { - final int version = Build.VERSION.SDK_INT; - - if (version >= 19) { - return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; - } else { -// if ((context.getApplicationInfo().flags & 1 << 27) == 1) { -// return true; -// } else { -// return false; -// } - return true; - } - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private static boolean checkOp(Context context, int op) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - try { - Class clazz = AppOpsManager.class; - Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class); - return AppOpsManager.MODE_ALLOWED == (int) method - .invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else { - Log.e(TAG, "Below API 19 cannot invoke!"); - } - return false; - } - - /** - * 小米 ROM 权限申请 - */ - public static void applyMiuiPermission(Context context) { - int versionCode = getMiuiVersion(); - if (versionCode == 5) { - goToMiuiPermissionActivity_V5(context); - } else if (versionCode == 6) { - goToMiuiPermissionActivity_V6(context); - } else if (versionCode == 7) { - goToMiuiPermissionActivity_V7(context); - } else if (versionCode == 8) { - goToMiuiPermissionActivity_V8(context); - } else { - Log.e(TAG, "this is a special MIUI rom version, its version code " + versionCode); - } - } - - private static boolean isIntentAvailable(Intent intent, Context context) { - if (intent == null) { - return false; - } - return - context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) - .size() > 0; - } - - /** - * 小米 V5 版本 ROM权限申请 - */ - public static void goToMiuiPermissionActivity_V5(Context context) { - Intent intent = null; - String packageName = context.getPackageName(); - intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", packageName, null); - intent.setData(uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - Log.e(TAG, "intent is not available!"); - } - - //设置页面在应用详情页面 -// Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); -// PackageInfo pInfo = null; -// try { -// pInfo = context.getPackageManager().getPackageInfo -// (HostInterfaceManager.getHostInterface().getApp().getPackageName(), 0); -// } catch (PackageManager.NameNotFoundException e) { -// AVLogUtils.e(TAG, e.getMessage()); -// } -// intent.setClassName("com.android.settings", "com.miui.securitycenter.permission.AppPermissionsEditor"); -// intent.putExtra("extra_package_uid", pInfo.applicationInfo.uid); -// intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); -// if (isIntentAvailable(intent, context)) { -// context.startActivity(intent); -// } else { -// AVLogUtils.e(TAG, "Intent is not available!"); -// } - } - - /** - * 小米 V6 版本 ROM权限申请 - */ - public static void goToMiuiPermissionActivity_V6(Context context) { - Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); - intent.setClassName("com.miui.securitycenter", - "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); - intent.putExtra("extra_pkgname", context.getPackageName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - Log.e(TAG, "Intent is not available!"); - } - } - - /** - * 小米 V7 版本 ROM权限申请 - */ - public static void goToMiuiPermissionActivity_V7(Context context) { - Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); - intent.setClassName("com.miui.securitycenter", - "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); - intent.putExtra("extra_pkgname", context.getPackageName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - Log.e(TAG, "Intent is not available!"); - } - } - - /** - * 小米 V8 版本 ROM权限申请 - */ - public static void goToMiuiPermissionActivity_V8(Context context) { - Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); - intent.setClassName("com.miui.securitycenter", - "com.miui.permcenter.permissions.PermissionsEditorActivity"); -// intent.setPackage("com.miui.securitycenter"); - intent.putExtra("extra_pkgname", context.getPackageName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); - intent.setPackage("com.miui.securitycenter"); - intent.putExtra("extra_pkgname", context.getPackageName()); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - Log.e(TAG, "Intent is not available!"); - } - } - } -} diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/QikuUtils.java b/app/src/main/java/remix/myplayer/misc/floatpermission/rom/QikuUtils.java deleted file mode 100644 index d2ae63cb4..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/QikuUtils.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission.rom; - -import android.annotation.TargetApi; -import android.app.AppOpsManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Binder; -import android.os.Build; -import android.util.Log; -import java.lang.reflect.Method; - -public class QikuUtils { - - private static final String TAG = "QikuUtils"; - - /** - * 检测 360 悬浮窗权限 - */ - public static boolean checkFloatWindowPermission(Context context) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - return checkOp(context, 24); //OP_SYSTEM_ALERT_WINDOW = 24; - } - return true; - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private static boolean checkOp(Context context, int op) { - final int version = Build.VERSION.SDK_INT; - if (version >= 19) { - AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); - try { - Class clazz = AppOpsManager.class; - Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class); - return AppOpsManager.MODE_ALLOWED == (int) method - .invoke(manager, op, Binder.getCallingUid(), context.getPackageName()); - } catch (Exception e) { - Log.e(TAG, Log.getStackTraceString(e)); - } - } else { - Log.e("", "Below API 19 cannot invoke!"); - } - return false; - } - - /** - * 去360权限申请页面 - */ - public static void applyPermission(Context context) { - Intent intent = new Intent(); - intent.setClassName("com.android.settings", - "com.android.settings.Settings$OverlaySettingsActivity"); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - intent.setClassName("com.qihoo360.mobilesafe", - "com.qihoo360.mobilesafe.ui.index.AppEnterActivity"); - if (isIntentAvailable(intent, context)) { - context.startActivity(intent); - } else { - Log.e(TAG, "can't open permission page with particular name, please use " + - "\"adb shell dumpsys activity\" command and tell me the name of the float window permission page"); - } - } - } - - private static boolean isIntentAvailable(Intent intent, Context context) { - if (intent == null) { - return false; - } - return - context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) - .size() > 0; - } -} diff --git a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/RomUtils.java b/app/src/main/java/remix/myplayer/misc/floatpermission/rom/RomUtils.java deleted file mode 100644 index d85e836dd..000000000 --- a/app/src/main/java/remix/myplayer/misc/floatpermission/rom/RomUtils.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved. - */ -package remix.myplayer.misc.floatpermission.rom; - -import android.os.Build; -import android.text.TextUtils; -import android.util.Log; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * Description: - * - * @author zhaozp - * @since 2016-05-23 - */ -public class RomUtils { - - private static final String TAG = "RomUtils"; - - /** - * 获取 emui 版本号 - */ - public static double getEmuiVersion() { - try { - String emuiVersion = getSystemProperty("ro.build.version.emui"); - String version = emuiVersion.substring(emuiVersion.indexOf("_") + 1); - return Double.parseDouble(version); - } catch (Exception e) { - e.printStackTrace(); - } - return 4.0; - } - - /** - * 获取小米 rom 版本号,获取失败返回 -1 - * - * @return miui rom version code, if fail , return -1 - */ - public static int getMiuiVersion() { - String version = getSystemProperty("ro.miui.ui.version.name"); - if (version != null) { - try { - return Integer.parseInt(version.substring(1)); - } catch (Exception e) { - Log.e(TAG, "get miui version code error, version : " + version); - } - } - return -1; - } - - public static String getSystemProperty(String propName) { - String line; - BufferedReader input = null; - try { - Process p = Runtime.getRuntime().exec("getprop " + propName); - input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); - line = input.readLine(); - input.close(); - } catch (IOException ex) { - Log.e(TAG, "Unable to read sysprop " + propName, ex); - return null; - } finally { - if (input != null) { - try { - input.close(); - } catch (IOException e) { - Log.e(TAG, "Exception while closing InputStream", e); - } - } - } - return line; - } - - public static boolean checkIsHuaweiRom() { - return Build.MANUFACTURER.contains("HUAWEI"); - } - - /** - * check if is miui ROM - */ - public static boolean checkIsMiuiRom() { - return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name")); - } - - public static boolean checkIsMeizuRom() { - return Build.DISPLAY.toLowerCase().contains("flyme"); - } - - public static boolean checkIs360Rom() { - //fix issue https://github.com/zhaozepeng/FloatWindowPermission/issues/9 - return Build.MANUFACTURER.contains("QiKU") - || Build.MANUFACTURER.contains("360"); - } - - public static boolean checkIsexTHmUIRom() { - String exTHmUIFlag = getSystemProperty("ro.exthm.device"); - if (TextUtils.isEmpty(exTHmUIFlag)) - return false; - else - return true; - } - - public static boolean checkIsbaolong24Rom() { - String baolong24Flag = getSystemProperty("org.baolong24.device"); - if (TextUtils.isEmpty(baolong24Flag)) - return false; - else - return true; - } -} diff --git a/app/src/main/java/remix/myplayer/misc/manager/APlayerActivityManager.kt b/app/src/main/java/remix/myplayer/misc/manager/APlayerActivityManager.kt index d9b732714..a65899e71 100644 --- a/app/src/main/java/remix/myplayer/misc/manager/APlayerActivityManager.kt +++ b/app/src/main/java/remix/myplayer/misc/manager/APlayerActivityManager.kt @@ -3,6 +3,7 @@ package remix.myplayer.misc.manager import android.app.Activity import android.app.Application import android.os.Bundle +import remix.myplayer.lyrics.LyricsManager import java.lang.ref.WeakReference import java.util.* @@ -30,6 +31,10 @@ open class APlayerActivityManager :Application.ActivityLifecycleCallbacks{ } private var foregroundActivityCount = 0 + set(value) { + field = value + LyricsManager.isAppInForeground = isAppForeground + } val isAppForeground: Boolean get() = foregroundActivityCount > 0 } diff --git a/app/src/main/java/remix/myplayer/misc/menu/AudioPopupListener.kt b/app/src/main/java/remix/myplayer/misc/menu/AudioPopupListener.kt index ba0daff4e..0c7abd7da 100644 --- a/app/src/main/java/remix/myplayer/misc/menu/AudioPopupListener.kt +++ b/app/src/main/java/remix/myplayer/misc/menu/AudioPopupListener.kt @@ -1,7 +1,7 @@ package remix.myplayer.misc.menu +import android.content.ActivityNotFoundException import android.content.ContextWrapper -import android.content.Intent import android.view.MenuItem import android.webkit.MimeTypeMap import android.widget.CompoundButton @@ -13,20 +13,22 @@ import remix.myplayer.bean.mp3.Song import remix.myplayer.db.room.DatabaseRepository import remix.myplayer.helper.DeleteHelper import remix.myplayer.helper.EQHelper +import remix.myplayer.helper.LyricsHelper import remix.myplayer.helper.MusicServiceRemote.getCurrentSong +import remix.myplayer.lyrics.LyricsManager +import remix.myplayer.lyrics.provider.EmbeddedProvider +import remix.myplayer.lyrics.provider.IgnoredProvider +import remix.myplayer.lyrics.provider.StubProvider import remix.myplayer.service.Command import remix.myplayer.theme.Theme.getBaseDialog -import remix.myplayer.ui.ViewCommon import remix.myplayer.ui.activity.PlayerActivity import remix.myplayer.ui.dialog.AddtoPlayListDialog import remix.myplayer.ui.dialog.TimerDialog import remix.myplayer.ui.misc.AudioTag -import remix.myplayer.util.MusicUtil import remix.myplayer.util.RxUtil.applySingleScheduler import remix.myplayer.util.SPUtil import remix.myplayer.util.ToastUtil import remix.myplayer.util.Util -import remix.myplayer.util.Util.sendLocalBroadcast import java.lang.ref.WeakReference /** @@ -44,7 +46,7 @@ class AudioPopupListener(activity: PlayerActivity, private val song: Song) : val activity = ref.get() ?: return true when (item.itemId) { R.id.menu_lyric -> { - ViewCommon.showLocalLyricTip(activity) { + LyricsHelper.showLocalLyricsTip(activity) { onClickLyric(activity) } return true @@ -141,86 +143,40 @@ class AudioPopupListener(activity: PlayerActivity, private val song: Song) : } return true } - + private fun onClickLyric(activity: PlayerActivity) { - val alreadyIgnore = (SPUtil - .getValue( - ref.get(), SPUtil.LYRIC_KEY.NAME, song.id.toString(), - SPUtil.LYRIC_KEY.LYRIC_DEFAULT - ) == SPUtil.LYRIC_KEY.LYRIC_IGNORE) - - val lyricFragment = ref.get()?.lyricFragment ?: return - getBaseDialog(ref.get()) - .items( + getBaseDialog(activity).items( + getString(R.string.default_lyrics), getString(R.string.embedded_lyric), getString(R.string.local), getString(R.string.kugou), getString(R.string.netease), getString(R.string.qq), getString(R.string.select_lrc), - getString(if (!alreadyIgnore) R.string.ignore_lrc else R.string.cancel_ignore_lrc), + getString(R.string.ignore_lrc), getString(R.string.lyric_adjust_font_size), getString(R.string.change_offset) - ) - .itemsCallback { dialog, itemView, position, text -> + ).itemsCallback { _, _, position, _ -> when (position) { - 0, 1, 2, 3, 4 -> { //0内嵌 1本地 2酷狗 3网易 4qq - SPUtil.putValue(ref.get(), SPUtil.LYRIC_KEY.NAME, song.id.toString(), position + 2) - lyricFragment.updateLrc(song, true) - sendLocalBroadcast(MusicUtil.makeCmdIntent(Command.CHANGE_LYRIC)) - } - - 5 -> { //手动选择歌词 - val intent = Intent(Intent.ACTION_GET_CONTENT).apply { - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension("lrc") - addCategory(Intent.CATEGORY_OPENABLE) - } - Util.startActivityForResultSafely( - activity, - intent, - PlayerActivity.REQUEST_SELECT_LYRIC - ) - } - - 6 -> { //忽略或者取消忽略 - getBaseDialog(activity) - .title(if (!alreadyIgnore) R.string.confirm_ignore_lrc else R.string.confirm_cancel_ignore_lrc) - .negativeText(R.string.cancel) - .positiveText(R.string.confirm) - .onPositive { dialog1, which -> - if (!alreadyIgnore) {//忽略 - SPUtil.putValue( - activity, SPUtil.LYRIC_KEY.NAME, song.id.toString(), - SPUtil.LYRIC_KEY.LYRIC_IGNORE - ) - lyricFragment.updateLrc(song) - } else {//取消忽略 - SPUtil.putValue( - activity, SPUtil.LYRIC_KEY.NAME, song.id.toString(), - SPUtil.LYRIC_KEY.LYRIC_DEFAULT - ) - lyricFragment.updateLrc(song) - } - sendLocalBroadcast(MusicUtil.makeCmdIntent(Command.CHANGE_LYRIC)) - } - .show() - } - - 7 -> { //字体大小调整 - getBaseDialog(ref.get()) - .items(R.array.lyric_font_size) - .itemsCallback { dialog, itemView, position, text -> - lyricFragment.setLyricScalingFactor(position) - } - .show() - } - - 8 -> { //歌词时间轴调整 - activity.showLyricOffsetView() + 0 -> LyricsManager.updateLyrics(getCurrentSong(), StubProvider) // 恢复默认 + 1 -> LyricsManager.updateLyrics(getCurrentSong(), EmbeddedProvider) // 内嵌 + 2, 3, 4, 5 -> TODO() // 本地 酷狗 网易 QQ + 6 -> try { + activity.getContent.launch(lrcMimeType) // 手动选择 + } catch (e: ActivityNotFoundException) { + ToastUtil.show(activity, R.string.activity_not_found_tip) } + + 7 -> LyricsManager.updateLyrics(getCurrentSong(), IgnoredProvider) // 忽略 + 8 -> TODO() // 调整字体大小 + 9 -> activity.showLyricOffsetView() // 调整时间轴 } - - } - .show() + }.show() + // TODO: update LyricsFragment + } + + companion object { + private val lrcMimeType: String + get() = MimeTypeMap.getSingleton().getMimeTypeFromExtension("lrc") ?: "*/*" } } diff --git a/app/src/main/java/remix/myplayer/service/Command.java b/app/src/main/java/remix/myplayer/service/Command.java index 9a1a5107c..b361e2250 100644 --- a/app/src/main/java/remix/myplayer/service/Command.java +++ b/app/src/main/java/remix/myplayer/service/Command.java @@ -17,11 +17,11 @@ public interface Command { int UNLOCK_DESKTOP_LYRIC = 11; int CLOSE_NOTIFY = 12; int ADD_TO_NEXT_SONG = 13; - int CHANGE_LYRIC = 14; +// int CHANGE_LYRIC = 14; int PLAY_AT_BREAKPOINT = 15; int TOGGLE_TIMER = 16; int TOGGLE_DESKTOP_LYRIC = 17; int HEADSET_CHANGE = 18; - int LOCK_DESKTOP_LYRIC = 19; - int TOGGLE_STATUS_BAR_LRC = 20; +// int LOCK_DESKTOP_LYRIC = 19; +// int TOGGLE_STATUS_BAR_LRC = 20; } diff --git a/app/src/main/java/remix/myplayer/service/MusicService.kt b/app/src/main/java/remix/myplayer/service/MusicService.kt index 4fc1b6966..bf03545f2 100644 --- a/app/src/main/java/remix/myplayer/service/MusicService.kt +++ b/app/src/main/java/remix/myplayer/service/MusicService.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.app.PendingIntent import android.content.* import android.graphics.Bitmap -import android.graphics.PixelFormat import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.media.AudioAttributes @@ -12,19 +11,13 @@ import android.media.AudioManager import android.media.MediaMetadataRetriever import android.media.MediaPlayer import android.media.PlaybackParams -import android.net.Uri import android.os.* import android.provider.MediaStore -import android.provider.Settings import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.text.TextUtils -import android.view.Gravity -import android.view.ViewGroup -import android.view.WindowManager import androidx.annotation.WorkerThread -import androidx.core.app.ServiceCompat import androidx.media.AudioAttributesCompat import androidx.media.AudioFocusRequestCompat import androidx.media.AudioManagerCompat @@ -45,10 +38,7 @@ import remix.myplayer.bean.mp3.Song.Companion.EMPTY_SONG import remix.myplayer.db.room.DatabaseRepository import remix.myplayer.db.room.model.PlayQueue import remix.myplayer.helper.* -import remix.myplayer.lyric.LyricFetcher -import remix.myplayer.lyric.LyricFetcher.Companion.LYRIC_FIND_INTERVAL -import remix.myplayer.lyric.bean.LyricRowWrapper -import remix.myplayer.misc.floatpermission.FloatWindowManager +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.misc.getPendingIntentFlag import remix.myplayer.misc.log.LogObserver import remix.myplayer.misc.observer.MediaStoreObserver @@ -66,7 +56,6 @@ import remix.myplayer.ui.activity.LockScreenActivity import remix.myplayer.ui.activity.base.BaseMusicActivity import remix.myplayer.ui.activity.base.BaseMusicActivity.Companion.EXTRA_PERMISSION import remix.myplayer.ui.activity.base.BaseMusicActivity.Companion.EXTRA_PLAYLIST -import remix.myplayer.ui.widget.desktop.DesktopLyricView import remix.myplayer.util.* import remix.myplayer.util.Constants.* import remix.myplayer.util.RxUtil.applySingleScheduler @@ -78,6 +67,7 @@ import remix.myplayer.util.Util.unregisterLocalReceiver import timber.log.Timber import java.lang.ref.WeakReference import java.util.* +import kotlin.time.Duration.Companion.milliseconds /** * Created by Remix on 2015/12/1. @@ -249,32 +239,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, */ private var control: Int = 0 - /** - * WindowManager 控制悬浮窗 - */ - private val windowManager: WindowManager by lazy { - getSystemService(Context.WINDOW_SERVICE) as WindowManager - } - - /** - * 是否显示状态栏歌词 - */ - private var showStatusBarLyric = false - - /** - * 是否显示桌面歌词 - */ - private var showDesktopLyric = false - set(value) { -// Timber.v(Throwable("设置桌面歌词开关, old: $field new: $value")) - field = value - } - - /** - * 桌面歌词控件 - */ - private var desktopLyricView: DesktopLyricView? = null - /** * service是否停止运行 */ @@ -287,6 +251,10 @@ class MusicService : BaseService(), Playback, MusicEventCallback, ScreenReceiver() } private var screenOn = true + set(value) { + field = value + LyricsManager.isScreenOn = value + } /** * 音量控制 @@ -367,31 +335,10 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } else 0 /** - * 更新桌面歌词与桌面部件 + * 保存当前进度&更新桌面部件 */ private var timer: Timer = Timer() private var desktopWidgetTask: WidgetTask? = null - private var lyricTask: LyricTask? = null - - /** - * 创建桌面歌词悬浮窗 - */ - private var isDesktopLyricInitializing = false - - /** - * 桌面歌词是否显示 - */ - val isDesktopLyricShowing: Boolean - get() = desktopLyricView != null - - /** - * 桌面歌词是否锁定 - */ - val isDesktopLyricLocked: Boolean - get() = if (desktopLyricView == null) - SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_LOCK, false) - else - desktopLyricView?.isLocked == true /** * 锁屏 @@ -575,6 +522,8 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } }) + LyricsManager.isServiceAvailable = true + setUpPlayer() setUpSession() } @@ -624,23 +573,14 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } override fun onSeekTo(pos: Long) { - setProgress(pos.toInt()) + setProgress(pos) } override fun onCustomAction(action: String?, extras: Bundle?) { - Timber.v("onCustomAction, ac: $action extra: $extras") - val intent = Intent(ACTION_CMD) - when(action) { - ACTION_UNLOCK_DESKTOP_LYRIC -> { - intent.putExtra(EXTRA_CONTROL, Command.UNLOCK_DESKTOP_LYRIC) - } - ACTION_TOGGLE_DESKTOP_LYRIC -> { - intent.putExtra(EXTRA_CONTROL, Command.TOGGLE_DESKTOP_LYRIC) - } - } - if (intent.hasExtra(EXTRA_CONTROL)) { - handleCommand(intent) - updatePlaybackState() + Timber.v("onCustomAction, ac: $action extra: $extras") + when (action) { + ACTION_UNLOCK_DESKTOP_LYRIC -> LyricsManager.isDesktopLyricsLocked = false + ACTION_TOGGLE_DESKTOP_LYRIC -> LyricsManager.setDesktopLyricsEnabled(!LyricsManager.isDesktopLyricsEnabled) } } }) @@ -754,11 +694,10 @@ class MusicService : BaseService(), Playback, MusicEventCallback, timer.cancel() notify.cancelPlayingNotify() - removeDesktopLyric() + LyricsManager.isServiceAvailable = false uiHandler.removeCallbacksAndMessages(null) - showDesktopLyric = false - lyricTask?.cancel() + uiHandler.sendEmptyMessage(UPDATE_NOTIFICATION) AudioManagerCompat.abandonAudioFocusRequest(audioManager, focusRequest) @@ -1167,11 +1106,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, updateAppwidget() updateNotification() updateMediaSession(operation) - //桌面歌词与状态栏歌词 - if (lyricTask == null) { - lyricTask = LyricTask() - timer.schedule(lyricTask, LYRIC_FIND_INTERVAL, LYRIC_FIND_INTERVAL) - } // 是否需要保存进度 if (playAtBreakPoint) { startSaveProgress() @@ -1180,11 +1114,11 @@ class MusicService : BaseService(), Playback, MusicEventCallback, sendLocalBroadcast(Intent(META_CHANGE)) } - private fun updateNotification() { + fun updateNotification() { notify.updateForPlaying() } - private fun updateNotificationWithLrc(lrc: String) { + fun updateNotificationWithLrc(lrc: String) { notify.updateWithLyric(lrc) } @@ -1192,8 +1126,7 @@ class MusicService : BaseService(), Playback, MusicEventCallback, if (playQueue.song == EMPTY_SONG) { return } - //更新桌面歌词播放按钮 - desktopLyricView?.setPlayIcon(isPlaying) + LyricsManager.isPlaying = isPlaying sendLocalBroadcast(Intent(PLAY_STATE_CHANGE)) } @@ -1245,7 +1178,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, // return; // } pause(false) - uiHandler.sendEmptyMessage(REMOVE_DESKTOP_LRC) uiHandler.postDelayed({ notify.cancelPlayingNotify() }, 300) } //播放选中的歌曲 @@ -1291,32 +1223,7 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } //桌面歌词 Command.TOGGLE_DESKTOP_LYRIC -> { - val open: Boolean = if (intent.hasExtra(EXTRA_DESKTOP_LYRIC)) { - intent.getBooleanExtra(EXTRA_DESKTOP_LYRIC, false) - } else { - !SPUtil.getValue(service, - SETTING_KEY.NAME, - SETTING_KEY.DESKTOP_LYRIC_SHOW, false) - } - if (open && !FloatWindowManager.getInstance().checkPermission(service)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val permissionIntent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) - permissionIntent.data = Uri.parse("package:$packageName") - permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - Util.startActivitySafely(service, permissionIntent) - } - ToastUtil.show(service, R.string.plz_give_float_permission) - return - } - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_SHOW, open) - if (showDesktopLyric != open) { - showDesktopLyric = open - ToastUtil.show(service, if (showDesktopLyric) R.string.opened_desktop_lrc else R.string.closed_desktop_lrc) - } - } - // 状态栏歌词 - Command.TOGGLE_STATUS_BAR_LRC -> { - showStatusBarLyric = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.STATUSBAR_LYRIC_SHOW, false) + LyricsManager.setDesktopLyricsEnabled(!LyricsManager.isDesktopLyricsEnabled) } //临时播放一首歌曲 Command.PLAY_TEMP -> { @@ -1328,19 +1235,7 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } //解锁桌面歌词 Command.UNLOCK_DESKTOP_LYRIC -> { - if (desktopLyricView != null) { - desktopLyricView?.saveLock(false, true) - } else { - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_LOCK, false) - } - //更新通知栏 - updateNotification() - } - //锁定桌面歌词,更新通知栏 - Command.LOCK_DESKTOP_LYRIC -> { - //更新通知栏 - updateNotification() - updatePlaybackState() + LyricsManager.isDesktopLyricsLocked = false } //某一首歌曲添加至下一首播放 Command.ADD_TO_NEXT_SONG -> { @@ -1349,14 +1244,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, playQueue.addNextSong(nextSong) ToastUtil.show(service, R.string.already_add_to_next_song) } - //改变歌词源 - Command.CHANGE_LYRIC -> { - lyricTask?.force = true -// if (showDesktopLyric) { -// updateDesktopLyric(true) -// } -// updateStatusBarLyric(true) - } //切换定时器 Command.TOGGLE_TIMER -> { val hasDefault = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.TIMER_DEFAULT, false) @@ -1448,8 +1335,8 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } - private fun updatePlaybackState() { - val desktopLyricLock = isDesktopLyricLocked + fun updatePlaybackState() { + val desktopLyricLock = LyricsManager.isDesktopLyricsLocked val builder = PlaybackStateCompat.Builder() builder.setActiveQueueItemId(currentSong.id) @@ -1473,6 +1360,7 @@ class MusicService : BaseService(), Playback, MusicEventCallback, * @param song 播放歌曲的路径 */ private fun prepare(song: Song, requestFocus: Boolean = true) { + LyricsManager.updateLyrics(song, null) tryLaunch( block = { Timber.v("prepare start: %s", song) @@ -1570,9 +1458,16 @@ class MusicService : BaseService(), Playback, MusicEventCallback, /** * 设置MediaPlayer播放进度 */ - fun setProgress(current: Int) { + fun setProgress(current: Long) { if (prepared) { - mediaPlayer.seekTo(current) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mediaPlayer.seekTo(current, MediaPlayer.SEEK_PREVIOUS_SYNC) + } else { + mediaPlayer.seekTo(current.toInt()) + } + launch(Dispatchers.IO) { + LyricsManager.updateProgress() + } updatePlaybackState() } } @@ -1624,8 +1519,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, //用户设置 lockScreen = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.LOCKSCREEN, APLAYER_LOCKSCREEN) playModel = SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.PLAY_MODEL, MODE_LOOP) - showDesktopLyric = SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_SHOW, false) - showStatusBarLyric = SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.STATUSBAR_LYRIC_SHOW, false); speed = java.lang.Float.parseFloat(SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.SPEED, "1.0")) playAtBreakPoint = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.PLAY_AT_BREAKPOINT, false) lastProgress = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.LAST_PLAY_PROGRESS, 0) @@ -1662,23 +1555,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, wakeLock.acquire(if (playQueue.song != EMPTY_SONG) playQueue.song.duration else 30000L) } - /** - * 判断是否有悬浮窗权限 没有权限关闭桌面歌词 - */ - private fun checkNoPermission(): Boolean { - try { - if (!FloatWindowManager.getInstance().checkPermission(service)) { - closeDesktopLyric() - return true - } - return false - } catch (e: Exception) { - Timber.v(e) - } - - return true - } - /** * 更新桌面部件 */ @@ -1734,132 +1610,12 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } } - fun setLyricOffset(offset: Int) { - lyricTask?.lyricFetcher?.offset = offset - } - - private inner class LyricTask : TimerTask() { - private var prev: CharSequence = "" - val lyricFetcher = LyricFetcher(this@MusicService) - var force = false - - override fun run() { - if (!showDesktopLyric) { - if (isDesktopLyricShowing) { - uiHandler.sendEmptyMessage(REMOVE_DESKTOP_LRC) - } - if (!showStatusBarLyric || stop) { - return - } - } - - val currentSong = playQueue.song - if (lyricFetcher.song != currentSong || force) { - Timber.tag(TAG_DESKTOP_LYRIC).v("重新获取歌词内容, id: ${currentSong.id}") - force = false - lyricFetcher.updateLyricRows(currentSong) - return - } - - // 桌面歌词 - val wrapper = lyricFetcher.findCurrentLyric() - Timber.tag(TAG_DESKTOP_LYRIC).v("findCurrentLyric: $wrapper") - if (!showDesktopLyric || !screenOn || checkNoPermission() || isAppOnForeground) { - if (isDesktopLyricShowing) { - Timber.tag(TAG_DESKTOP_LYRIC).v("remove desktop lyric") - uiHandler.sendEmptyMessage(REMOVE_DESKTOP_LRC) - } - } else { - if (!isDesktopLyricShowing) { - Timber.tag(TAG_DESKTOP_LYRIC).v("create desktop lyric") - uiHandler.removeMessages(CREATE_DESKTOP_LRC) - uiHandler.sendEmptyMessageDelayed(CREATE_DESKTOP_LRC, 50) - } else { - Timber.tag(TAG_DESKTOP_LYRIC).v("update desktop lyric") - uiHandler.obtainMessage(UPDATE_DESKTOP_LRC_CONTENT, wrapper).sendToTarget() - } - } - - // 状态栏歌词 - if (showStatusBarLyric) { - if (TextUtils.equals(prev, wrapper.lineOne.content)) { - return - } - prev = wrapper.lineOne.content - uiHandler.obtainMessage(UPDATE_STATUS_BAR_LRC, wrapper).sendToTarget() - } - } - - override fun cancel(): Boolean { - Timber.tag(TAG_DESKTOP_LYRIC).v("cancel task") - lyricFetcher.dispose() - uiHandler.sendEmptyMessage(UPDATE_NOTIFICATION) - return super.cancel() - } - } - - private fun createDesktopLyric() { - if (checkNoPermission()) { - return - } - if (isDesktopLyricInitializing) { - return - } - isDesktopLyricInitializing = true - - val param = WindowManager.LayoutParams() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - param.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - } else { - param.type = WindowManager.LayoutParams.TYPE_PHONE - } - - param.format = PixelFormat.RGBA_8888 - param.gravity = Gravity.TOP - param.width = resources.displayMetrics.widthPixels - param.height = ViewGroup.LayoutParams.WRAP_CONTENT - param.x = 0 - param.y = SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_Y, 0) - - if (desktopLyricView != null) { - windowManager.removeView(desktopLyricView) - desktopLyricView = null - } - - desktopLyricView = DesktopLyricView(service) - windowManager.addView(desktopLyricView, param) - isDesktopLyricInitializing = false - Timber.tag(TAG_DESKTOP_LYRIC).v("创建桌面歌词") - } - - /** - * 移除桌面歌词 - */ - private fun removeDesktopLyric() { - if (desktopLyricView != null) { - Timber.tag(TAG_DESKTOP_LYRIC).v("移除桌面歌词") - // desktopLyricView.cancelNotify(); - windowManager.removeView(desktopLyricView) - desktopLyricView = null - } - } - - /** - * 关闭桌面歌词 - */ - private fun closeDesktopLyric() { - SPUtil.putValue(this, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_SHOW, false) - showDesktopLyric = false - uiHandler.removeMessages(CREATE_DESKTOP_LRC) - uiHandler.sendEmptyMessage(REMOVE_DESKTOP_LRC) - } - private fun startSaveProgress() { if (progressTask != null) { return } progressTask = ProgressTask() - timer.schedule(progressTask, 1000, LYRIC_FIND_INTERVAL) + timer.schedule(progressTask, 1000, SAVE_PROGRESS_INTERVAL) } private fun stopSaveProgress() { @@ -1944,24 +1700,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, // musicService.handlePlayStateChange() musicService.handleMetaChange() } - UPDATE_DESKTOP_LRC_CONTENT -> { - if (msg.obj is LyricRowWrapper) { - val wrapper = msg.obj as LyricRowWrapper - musicService.desktopLyricView?.setText(wrapper.lineOne, wrapper.lineTwo) - } - } - REMOVE_DESKTOP_LRC -> { - musicService.removeDesktopLyric() - } - CREATE_DESKTOP_LRC -> { - musicService.createDesktopLyric() - } - UPDATE_STATUS_BAR_LRC -> { - if (msg.obj is LyricRowWrapper) { - val wrapper = msg.obj as LyricRowWrapper - musicService.updateNotificationWithLrc(wrapper.lineOne.content); - } - } UPDATE_NOTIFICATION -> { musicService.updateNotification() } @@ -1996,9 +1734,7 @@ class MusicService : BaseService(), Playback, MusicEventCallback, } companion object { - const val TAG_DESKTOP_LYRIC = "LyricTask" const val TAG_LIFECYCLE = "ServiceLifeCycle" - const val EXTRA_DESKTOP_LYRIC = "DesktopLyric" const val EXTRA_SONG = "Song" const val EXTRA_POSITION = "Position" @@ -2011,18 +1747,6 @@ class MusicService : BaseService(), Playback, MusicEventCallback, //更新播放状态 const val UPDATE_PLAY_STATE = 1003 - //更新桌面歌词内容 - const val UPDATE_DESKTOP_LRC_CONTENT = 1004 - - //移除桌面歌词 - const val REMOVE_DESKTOP_LRC = 1005 - - //添加桌面歌词 - const val CREATE_DESKTOP_LRC = 1006 - - //更新状态栏歌词 - const val UPDATE_STATUS_BAR_LRC = 1007 - //更新通知 const val UPDATE_NOTIFICATION = 1008 @@ -2074,6 +1798,8 @@ class MusicService : BaseService(), Playback, MusicEventCallback, private const val APPWIDGET_SMALL_TRANSPARENT = "AppWidgetSmallTransparent" private const val INTERVAL_APPWIDGET = 1000L + private const val SAVE_PROGRESS_INTERVAL = 500L + private val UPDATE_LYRICS_INTERVAL = 50.milliseconds // TODO: 是否合适? /** diff --git a/app/src/main/java/remix/myplayer/service/notification/Notify.kt b/app/src/main/java/remix/myplayer/service/notification/Notify.kt index 8bb2f8f91..1817af1a4 100644 --- a/app/src/main/java/remix/myplayer/service/notification/Notify.kt +++ b/app/src/main/java/remix/myplayer/service/notification/Notify.kt @@ -16,6 +16,7 @@ import androidx.core.app.ServiceCompat import androidx.core.app.TaskStackBuilder import com.bumptech.glide.request.target.CustomTarget import remix.myplayer.R +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.misc.getPendingIntentFlag import remix.myplayer.service.Command import remix.myplayer.service.MusicService @@ -137,35 +138,27 @@ abstract class Notify internal constructor(internal var service: MusicService) { intent.putExtra(EXTRA_CONTROL, operation) intent.component = ComponentName(context, MusicService::class.java) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return PendingIntent.getService(context, operation, intent, - getPendingIntentFlag() - ) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !listOf( + Command.TOGGLE_DESKTOP_LYRIC, + Command.CLOSE_NOTIFY, + Command.UNLOCK_DESKTOP_LYRIC + ).contains(operation) + ) { + PendingIntent.getForegroundService(context, operation, intent, getPendingIntentFlag()) } else { - if (operation != Command.TOGGLE_DESKTOP_LYRIC && - operation != Command.CLOSE_NOTIFY && - operation != Command.UNLOCK_DESKTOP_LYRIC) { - return PendingIntent.getForegroundService(context, operation, intent, - getPendingIntentFlag() - ) - } else { - PendingIntent.getService(context, operation, intent, - getPendingIntentFlag() - ) - } + PendingIntent.getService(context, operation, intent, getPendingIntentFlag()) } - - return PendingIntent.getService(context, operation, intent, - getPendingIntentFlag() - ) } companion object { /** * 通知栏是否显示 */ - @JvmStatic var isNotifyShowing = false + set(value) { + field = value + LyricsManager.isNotifyShowing = value + } private const val NOTIFY_MODE_FOREGROUND = 1 private const val NOTIFY_MODE_BACKGROUND = 2 diff --git a/app/src/main/java/remix/myplayer/service/notification/NotifyImpl.kt b/app/src/main/java/remix/myplayer/service/notification/NotifyImpl.kt index c0ace0998..251539ce3 100644 --- a/app/src/main/java/remix/myplayer/service/notification/NotifyImpl.kt +++ b/app/src/main/java/remix/myplayer/service/notification/NotifyImpl.kt @@ -13,6 +13,7 @@ import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.signature.ObjectKey import remix.myplayer.R import remix.myplayer.glide.UriFetcher +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.service.Command import remix.myplayer.service.MusicService import remix.myplayer.util.ColorUtil @@ -77,9 +78,9 @@ class NotifyImpl(context: MusicService) : Notify(context) { //桌面歌词 remoteBigView.setImageViewResource(R.id.notify_lyric, - if (service.isDesktopLyricLocked) R.drawable.icon_notify_desktop_lyric_unlock else R.drawable.icon_notify_lyric) + if (LyricsManager.isDesktopLyricsLocked) R.drawable.icon_notify_desktop_lyric_unlock else R.drawable.icon_notify_lyric) remoteView.setImageViewResource(R.id.notify_lyric, - if (service.isDesktopLyricLocked) R.drawable.icon_notify_desktop_lyric_unlock else R.drawable.icon_notify_lyric) + if (LyricsManager.isDesktopLyricsLocked) R.drawable.icon_notify_desktop_lyric_unlock else R.drawable.icon_notify_lyric) //设置播放按钮 if (!isPlay) { @@ -174,7 +175,7 @@ class NotifyImpl(context: MusicService) : Notify(context) { //桌面歌词 val lyricIntent = buildPendingIntent(context, - if (service.isDesktopLyricLocked) Command.UNLOCK_DESKTOP_LYRIC else Command.TOGGLE_DESKTOP_LYRIC) + if (LyricsManager.isDesktopLyricsLocked) Command.UNLOCK_DESKTOP_LYRIC else Command.TOGGLE_DESKTOP_LYRIC) remoteBigView.setOnClickPendingIntent(R.id.notify_lyric, lyricIntent) remoteView.setOnClickPendingIntent(R.id.notify_lyric, lyricIntent) } diff --git a/app/src/main/java/remix/myplayer/service/notification/NotifyImpl24.kt b/app/src/main/java/remix/myplayer/service/notification/NotifyImpl24.kt index 9eb49b81b..eaa597549 100644 --- a/app/src/main/java/remix/myplayer/service/notification/NotifyImpl24.kt +++ b/app/src/main/java/remix/myplayer/service/notification/NotifyImpl24.kt @@ -15,6 +15,7 @@ import com.bumptech.glide.signature.ObjectKey import remix.myplayer.R import remix.myplayer.bean.mp3.Song import remix.myplayer.glide.UriFetcher +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.service.Command import remix.myplayer.service.MusicService import remix.myplayer.service.MusicService.Companion.EXTRA_CONTROL @@ -66,10 +67,7 @@ NotifyImpl24(context: MusicService) : Notify(context) { val playPauseIcon = if (service.isPlaying) R.drawable.ic_pause_black_24dp else R.drawable.ic_play_arrow_black_24dp - val deleteIntent = Intent(MusicService.ACTION_CMD) - deleteIntent.putExtra(EXTRA_CONTROL, Command.CLOSE_NOTIFY) - - val desktopLyricLock = service.isDesktopLyricLocked + val desktopLyricLock = LyricsManager.isDesktopLyricsLocked val notification = NotificationCompat.Builder(service, PLAYING_NOTIFICATION_CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) diff --git a/app/src/main/java/remix/myplayer/theme/MaterialTintHelper.kt b/app/src/main/java/remix/myplayer/theme/MaterialTintHelper.kt new file mode 100644 index 000000000..164235a62 --- /dev/null +++ b/app/src/main/java/remix/myplayer/theme/MaterialTintHelper.kt @@ -0,0 +1,18 @@ +package remix.myplayer.theme + +import android.content.res.ColorStateList +import com.google.android.material.slider.Slider +import com.google.android.material.textfield.TextInputEditText + +/** + * Helper for theming Material Components + */ +object MaterialTintHelper { + fun setTint(slider: Slider) { +// TODO() + } + + fun setTint(editText: TextInputEditText) { +// TODO() + } +} diff --git a/app/src/main/java/remix/myplayer/theme/ThemeStore.kt b/app/src/main/java/remix/myplayer/theme/ThemeStore.kt index 843a17071..01141c22b 100644 --- a/app/src/main/java/remix/myplayer/theme/ThemeStore.kt +++ b/app/src/main/java/remix/myplayer/theme/ThemeStore.kt @@ -20,7 +20,6 @@ object ThemeStore { const val FOLLOW_SYSTEM = "follow_system" private const val KEY_PRIMARY_COLOR = "primary_color" private const val KEY_ACCENT_COLOR = "accent_color" - private const val KEY_FLOAT_LYRIC_TEXT_COLOR = "float_lyric_text_color" const val STATUS_BAR_ALPHA = 150 @JvmField @@ -282,23 +281,6 @@ object ThemeStore { } ) - @JvmStatic - @get:ColorInt - var floatLyricTextColor: Int - get() { - val temp = SPUtil.getValue( - App.context, KEY_NAME, KEY_FLOAT_LYRIC_TEXT_COLOR, materialPrimaryColor - ) - return if (ColorUtil.isColorCloseToWhite(temp)) { - Color.parseColor("#F9F9F9") - } else { - temp - } - } - set(value) { - SPUtil.putValue(App.context, KEY_NAME, KEY_FLOAT_LYRIC_TEXT_COLOR, value) - } - @get:ColorInt val colorOnPrimary: Int get() { diff --git a/app/src/main/java/remix/myplayer/ui/ViewCommon.kt b/app/src/main/java/remix/myplayer/ui/ViewCommon.kt deleted file mode 100644 index 59aab6f8f..000000000 --- a/app/src/main/java/remix/myplayer/ui/ViewCommon.kt +++ /dev/null @@ -1,31 +0,0 @@ -package remix.myplayer.ui - -import android.content.Context -import remix.myplayer.R -import remix.myplayer.theme.Theme -import remix.myplayer.util.SPUtil - -object ViewCommon { - fun showLocalLyricTip(context: Context, action: () -> Unit) { - if (!SPUtil.getValue( - context, - SPUtil.LYRIC_KEY.NAME, - SPUtil.LYRIC_KEY.LYRIC_LOCAL_TIP_SHOWN, - false - ) - ) { - SPUtil.putValue(context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.LYRIC_LOCAL_TIP_SHOWN, true) - Theme.getBaseDialog(context) - .negativeText(R.string.cancel) - .positiveText(R.string.confirm) - .onPositive { dialog, which -> - action.invoke() - } - .content(R.string.local_lyric_tip) - .show() - } else { - action.invoke() - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/ui/activity/LockScreenActivity.kt b/app/src/main/java/remix/myplayer/ui/activity/LockScreenActivity.kt index 27528bfdc..03a28bdb9 100644 --- a/app/src/main/java/remix/myplayer/ui/activity/LockScreenActivity.kt +++ b/app/src/main/java/remix/myplayer/ui/activity/LockScreenActivity.kt @@ -10,6 +10,7 @@ import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.view.animation.AnimationUtils +import androidx.annotation.UiThread import androidx.palette.graphics.Palette import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource @@ -20,14 +21,10 @@ import io.reactivex.Single import io.reactivex.disposables.Disposable import io.reactivex.functions.Consumer import remix.myplayer.R -import remix.myplayer.bean.mp3.Song import remix.myplayer.databinding.ActivityLockscreenBinding import remix.myplayer.helper.MusicServiceRemote -import remix.myplayer.lyric.LyricFetcher -import remix.myplayer.lyric.LyricFetcher.Companion.LYRIC_FIND_INTERVAL -import remix.myplayer.lyric.bean.LyricRowWrapper -import remix.myplayer.lyric.bean.LyricRowWrapper.Companion.LYRIC_WRAPPER_NO -import remix.myplayer.lyric.bean.LyricRowWrapper.Companion.LYRIC_WRAPPER_SEARCHING +import remix.myplayer.lyrics.CurrentNextLyricsLine +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.misc.menu.CtrlButtonListener import remix.myplayer.service.MusicService import remix.myplayer.ui.activity.base.BaseMusicActivity @@ -36,7 +33,6 @@ import remix.myplayer.util.ColorUtil import remix.myplayer.util.RxUtil import remix.myplayer.util.StatusBarUtil import timber.log.Timber -import java.lang.ref.WeakReference /** * Created by Remix on 2016/3/9. @@ -58,10 +54,6 @@ class LockScreenActivity : BaseMusicActivity() { private var disposable: Disposable? = null - @Volatile - private var curLyric: LyricRowWrapper? = null - private var updateLyricThread: UpdateLockScreenLyricThread? = null - //前后两次触摸的X private var scrollX1: Float = 0f private var scrollX2: Float = 0f @@ -111,6 +103,7 @@ class LockScreenActivity : BaseMusicActivity() { findViewById(R.id.lockscreen_arrow_container) .startAnimation(AnimationUtils.loadAnimation(this, R.anim.arrow_left_to_right)) + LyricsManager.setLockScreenActivity(this) } override fun onTouchEvent(event: MotionEvent): Boolean { @@ -153,10 +146,6 @@ class LockScreenActivity : BaseMusicActivity() { override fun onDestroy() { super.onDestroy() - if (updateLyricThread != null) { - updateLyricThread?.interrupt() - updateLyricThread = null - } disposable?.dispose() disposable = null } @@ -171,14 +160,6 @@ class LockScreenActivity : BaseMusicActivity() { override fun onMetaChanged() { super.onMetaChanged() val song = MusicServiceRemote.getCurrentSong() - //歌词 - if (updateLyricThread == null) { - val service = MusicServiceRemote.service - if (service != null) { - updateLyricThread = UpdateLockScreenLyricThread(this, service) - updateLyricThread?.start() - } - } //标题 binding.lockscreenSong.text = song.title @@ -254,52 +235,8 @@ class LockScreenActivity : BaseMusicActivity() { return Palette.from(rawBitMap ?: return null).generate() } - private fun setCurrentLyric(wrapper: LyricRowWrapper) { - runOnUiThread { - curLyric = wrapper - if (curLyric == null || curLyric === LYRIC_WRAPPER_NO) { - binding.lockscreenLyric.setTextWithAnimation(R.string.no_lrc) - } else if (curLyric === LYRIC_WRAPPER_SEARCHING) { - binding.lockscreenLyric.text = "" - } else { - binding.lockscreenLyric.setTextWithAnimation( - String.format("%s\n%s", curLyric?.lineOne?.content, - curLyric?.lineTwo?.content)) - } - - } - } - - private class UpdateLockScreenLyricThread constructor(activity: LockScreenActivity, service: MusicService) : Thread() { - - private val ref: WeakReference = WeakReference(activity) - private val lyricFetcher: LyricFetcher = LyricFetcher(service) - private var songInThread: Song = Song.EMPTY_SONG - - override fun interrupt() { - super.interrupt() - lyricFetcher.dispose() - } - - override fun run() { - while (true) { - try { - sleep(LYRIC_FIND_INTERVAL) - } catch (e: InterruptedException) { - return - } - - val song = MusicServiceRemote.getCurrentSong() - if (songInThread !== song) { - songInThread = song - lyricFetcher.updateLyricRows(songInThread) - continue - } - - val activity = ref.get() - activity?.setCurrentLyric(lyricFetcher.findCurrentLyric()) - } - } + @UiThread + fun setLyrics(lyrics: CurrentNextLyricsLine) { + binding.lockscreenLyric.setTextWithAnimation("${lyrics.currentLine?.content ?: ""}\n${lyrics.nextLine ?: ""}") } - } diff --git a/app/src/main/java/remix/myplayer/ui/activity/PlayerActivity.kt b/app/src/main/java/remix/myplayer/ui/activity/PlayerActivity.kt index 4a97b5339..b4442adf3 100644 --- a/app/src/main/java/remix/myplayer/ui/activity/PlayerActivity.kt +++ b/app/src/main/java/remix/myplayer/ui/activity/PlayerActivity.kt @@ -3,7 +3,6 @@ package remix.myplayer.ui.activity import android.animation.ArgbEvaluator import android.animation.ValueAnimator import android.annotation.SuppressLint -import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -26,7 +25,7 @@ import android.view.animation.Animation.AnimationListener import android.widget.ImageView import android.widget.LinearLayout import android.widget.SeekBar -import androidx.annotation.RequiresApi +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.FragmentManager import androidx.palette.graphics.Palette @@ -49,8 +48,8 @@ import remix.myplayer.helper.MusicServiceRemote.getPlayModel import remix.myplayer.helper.MusicServiceRemote.getProgress import remix.myplayer.helper.MusicServiceRemote.isPlaying import remix.myplayer.helper.MusicServiceRemote.setPlayModel -import remix.myplayer.lyric.LrcView -import remix.myplayer.lyric.LrcView.OnLrcClickListener +import remix.myplayer.lyrics.LyricsManager +import remix.myplayer.lyrics.provider.UriProvider import remix.myplayer.misc.cache.DiskCache import remix.myplayer.misc.handler.MsgHandler import remix.myplayer.misc.handler.OnHandleMessage @@ -68,10 +67,11 @@ import remix.myplayer.ui.adapter.PagerAdapter import remix.myplayer.ui.blur.StackBlurManager import remix.myplayer.ui.dialog.PlayQueueDialog import remix.myplayer.ui.dialog.PlayQueueDialog.Companion.newInstance -import remix.myplayer.ui.fragment.LyricFragment +import remix.myplayer.ui.fragment.LyricsFragment import remix.myplayer.ui.fragment.RecordFragment import remix.myplayer.ui.fragment.player.CoverFragment import remix.myplayer.ui.fragment.player.RoundCoverFragment +import remix.myplayer.ui.widget.LyricsView import remix.myplayer.util.* import remix.myplayer.util.SPUtil.SETTING_KEY import timber.log.Timber @@ -98,9 +98,6 @@ class PlayerActivity : BaseMusicActivity() { //是否正在拖动进度条 var isDragSeekBarFromUser = false - //歌词控件 - private var lrcView: LrcView? = null - //高亮与非高亮指示器 private lateinit var highLightIndicator: GradientDrawable private lateinit var normalIndicator: GradientDrawable @@ -119,7 +116,7 @@ class PlayerActivity : BaseMusicActivity() { private var duration = 0 //Fragment - lateinit var lyricFragment: LyricFragment + lateinit var lyricsFragment: LyricsFragment private set private lateinit var coverFragment: RoundCoverFragment @@ -165,6 +162,12 @@ class PlayerActivity : BaseMusicActivity() { SPUtil.getValue(this, SETTING_KEY.NAME, SETTING_KEY.PLAYER_BACKGROUND, BACKGROUND_ADAPTIVE_COLOR) } + val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { + LyricsManager.updateLyrics(getCurrentSong(), UriProvider(it)) + } + } + override fun setUpTheme() { // if (ThemeStore.isLightTheme()) { // super.setUpTheme(); @@ -451,7 +454,6 @@ class PlayerActivity : BaseMusicActivity() { } handler.sendEmptyMessage(UPDATE_TIME_ONLY) currentTime = progress - lrcView?.seekTo(progress, true, fromUser) } override fun onStartTrackingTouch(seekBar: SeekBar) { @@ -463,7 +465,7 @@ class PlayerActivity : BaseMusicActivity() { // if(!mIsPlay){ // seekBar.setProgress(0); // } - MusicServiceRemote.setProgress(seekBar.progress) + MusicServiceRemote.setProgress(seekBar.progress.toLong()) isDragSeekBarFromUser = false } }) @@ -477,8 +479,7 @@ class PlayerActivity : BaseMusicActivity() { val current = audioManager.getStreamVolume(STREAM_MUSIC) runOnUiThread { - if (min != 0) { - @RequiresApi(Build.VERSION_CODES.O) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { volumeSeekbar.min = min } volumeSeekbar.max = max @@ -563,7 +564,7 @@ class PlayerActivity : BaseMusicActivity() { val fragments = fragmentManager.fragments for (fragment in fragments) { - if (fragment is LyricFragment || + if (fragment is LyricsFragment || fragment is CoverFragment<*> || fragment is RecordFragment) { fragmentManager.beginTransaction().remove(fragment).commitNow() @@ -571,8 +572,8 @@ class PlayerActivity : BaseMusicActivity() { } coverFragment = RoundCoverFragment() setUpCoverFragment() - lyricFragment = LyricFragment() - setUpLyricFragment() + lyricsFragment = LyricsFragment() + setUpLyricsFragment() if (this.isPortraitOrientation()) { @@ -580,7 +581,7 @@ class PlayerActivity : BaseMusicActivity() { val adapter = PagerAdapter(supportFragmentManager) // adapter.addFragment(mRecordFragment); adapter.addFragment(coverFragment) - adapter.addFragment(lyricFragment) + adapter.addFragment(lyricsFragment) binding.viewPager.adapter = adapter binding.viewPager.offscreenPageLimit = adapter.count - 1 binding.viewPager.currentItem = 0 @@ -604,7 +605,7 @@ class PlayerActivity : BaseMusicActivity() { fragmentManager .beginTransaction() .replace(R.id.container_cover, coverFragment) - .replace(R.id.container_lyric, lyricFragment) + .replace(R.id.container_lyric, lyricsFragment) .commit() } @@ -653,27 +654,11 @@ class PlayerActivity : BaseMusicActivity() { } - private fun setUpLyricFragment() { - lyricFragment.setOnInflateFinishListener { view: View? -> - lrcView = view as LrcView - lrcView?.setOnLrcClickListener(object : OnLrcClickListener { - override fun onClick() {} - override fun onLongClick() {} - }) - - lrcView?.setOnSeekToListener(object : LrcView.OnSeekToListener { - override fun onSeekTo(progress: Int) { - if (progress > 0 && progress < getDuration()) { - MusicServiceRemote.setProgress(progress) - currentTime = progress - handler.sendEmptyMessage(UPDATE_TIME_ALL) - } - } - - }) - lrcView?.setHighLightColor(ThemeStore.textColorPrimary) - lrcView?.setOtherColor(ThemeStore.textColorSecondary) - lrcView?.setTimeLineColor(ThemeStore.textColorSecondary) + private fun setUpLyricsFragment() { + lyricsFragment.onSeekToListener = LyricsView.OnSeekToListener { progress -> + MusicServiceRemote.setProgress(progress) + currentTime = getProgress() + handler.sendEmptyMessage(UPDATE_TIME_ALL) } } @@ -691,7 +676,6 @@ class PlayerActivity : BaseMusicActivity() { super.onMediaStoreChanged() val newSong = getCurrentSong() updateTopStatus(newSong) - lyricFragment.updateLrc(newSong) song = newSong coverFragment.setImage(song, false, true) } @@ -704,8 +688,6 @@ class PlayerActivity : BaseMusicActivity() { if (operation != Command.TOGGLE || firstStart) { //更新顶部信息 updateTopStatus(song) - //更新歌词 - handler.postDelayed({ lyricFragment.updateLrc(song) }, 50) //更新进度条 val temp = getProgress() currentTime = if (temp in 1 until duration) temp else 0 @@ -967,22 +949,6 @@ class PlayerActivity : BaseMusicActivity() { Util.unregisterLocalReceiver(receiver) } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - if (requestCode == REQUEST_SELECT_LYRIC && resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - SPUtil.putValue( - this, - SPUtil.LYRIC_KEY.NAME, - song.id.toString(), - SPUtil.LYRIC_KEY.LYRIC_MANUAL - ) - lyricFragment.updateLrc(uri) - Util.sendLocalBroadcast(MusicUtil.makeCmdIntent(Command.CHANGE_LYRIC)) - } - } - } - private fun updateProgressText(current: Int) { if (current > 0 && duration - current > 0) { binding.textHasplay.text = Util.getTime(current.toLong()) @@ -1026,7 +992,7 @@ class PlayerActivity : BaseMusicActivity() { if (binding.viewPager.currentItem != 2) { binding.viewPager.setCurrentItem(2, true) } - lyricFragment.showLyricOffsetView() + lyricsFragment.showOffsetPanel() } private inner class Receiver : BroadcastReceiver() { @@ -1055,7 +1021,5 @@ class PlayerActivity : BaseMusicActivity() { private const val DELAY_SHOW_NEXT_SONG: Long = 3000 const val ACTION_UPDATE_NEXT = "remix.myplayer.update.next_song" - - const val REQUEST_SELECT_LYRIC = 0x104 } } \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/ui/activity/SettingActivity.kt b/app/src/main/java/remix/myplayer/ui/activity/SettingActivity.kt index 3118c7046..71301c8e0 100644 --- a/app/src/main/java/remix/myplayer/ui/activity/SettingActivity.kt +++ b/app/src/main/java/remix/myplayer/ui/activity/SettingActivity.kt @@ -13,7 +13,6 @@ import android.os.Bundle import android.os.Environment import android.os.Message import android.provider.MediaStore -import android.provider.Settings import android.view.View import android.webkit.MimeTypeMap import android.widget.CompoundButton @@ -47,15 +46,16 @@ import remix.myplayer.glide.UriFetcher.DOWNLOAD_LASTFM import remix.myplayer.helper.EQHelper import remix.myplayer.helper.LanguageHelper import remix.myplayer.helper.LanguageHelper.AUTO +import remix.myplayer.helper.LyricsHelper import remix.myplayer.helper.M3UHelper.exportPlayListToFile import remix.myplayer.helper.M3UHelper.importLocalPlayList import remix.myplayer.helper.M3UHelper.importM3UFile import remix.myplayer.helper.ShakeDetector +import remix.myplayer.lyrics.LyricsManager import remix.myplayer.misc.AppInfo import remix.myplayer.misc.MediaScanner import remix.myplayer.misc.SystemInfo import remix.myplayer.misc.cache.DiskCache -import remix.myplayer.misc.floatpermission.FloatWindowManager import remix.myplayer.misc.handler.MsgHandler import remix.myplayer.misc.handler.OnHandleMessage import remix.myplayer.misc.receiver.HeadsetPlugReceiver @@ -64,14 +64,11 @@ import remix.myplayer.misc.update.UpdateAgent import remix.myplayer.misc.update.UpdateListener import remix.myplayer.misc.zipFrom import remix.myplayer.misc.zipOutputStream -import remix.myplayer.service.Command import remix.myplayer.service.MusicService -import remix.myplayer.service.MusicService.Companion.EXTRA_DESKTOP_LYRIC import remix.myplayer.theme.Theme import remix.myplayer.theme.Theme.getBaseDialog import remix.myplayer.theme.ThemeStore import remix.myplayer.theme.TintHelper -import remix.myplayer.ui.ViewCommon import remix.myplayer.ui.activity.MainActivity.Companion.EXTRA_LIBRARY import remix.myplayer.ui.activity.MainActivity.Companion.EXTRA_RECREATE import remix.myplayer.ui.activity.MainActivity.Companion.EXTRA_REFRESH_ADAPTER @@ -79,16 +76,17 @@ import remix.myplayer.ui.activity.MainActivity.Companion.EXTRA_REFRESH_LIBRARY import remix.myplayer.ui.activity.PlayerActivity.Companion.BACKGROUND_ADAPTIVE_COLOR import remix.myplayer.ui.activity.PlayerActivity.Companion.BACKGROUND_CUSTOM_IMAGE import remix.myplayer.ui.activity.PlayerActivity.Companion.BACKGROUND_THEME -import remix.myplayer.ui.dialog.LyricPriorityDialog +import remix.myplayer.ui.dialog.LyricsOrderDialog import remix.myplayer.ui.dialog.color.ColorChooserDialog import remix.myplayer.ui.misc.FolderChooser import remix.myplayer.util.* import remix.myplayer.util.Constants.KB import remix.myplayer.util.Constants.MB import remix.myplayer.util.RxUtil.applySingleScheduler +import remix.myplayer.util.SPUtil.LYRICS_KEY import remix.myplayer.util.SPUtil.SETTING_KEY import remix.myplayer.util.SPUtil.SETTING_KEY.BOTTOM_OF_NOW_PLAYING_SCREEN -import remix.myplayer.util.Util.isSupportStatusBarLyric +import remix.myplayer.util.Util.isStatusBarLyricsSupported import remix.myplayer.util.Util.sendLocalBroadcast import timber.log.Timber import java.io.File @@ -101,7 +99,7 @@ import java.io.File */ //todo 重构整个界面 class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, - SharedPreferences.OnSharedPreferenceChangeListener { + SharedPreferences.OnSharedPreferenceChangeListener, OnCheckedChangeListener { private lateinit var binding: ActivitySettingBinding private lateinit var checkedChangedListener: OnCheckedChangeListener @@ -147,8 +145,6 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, val keyWord = arrayOf( SETTING_KEY.COLOR_NAVIGATION, SETTING_KEY.SHAKE, - SETTING_KEY.DESKTOP_LYRIC_SHOW, - SETTING_KEY.STATUSBAR_LYRIC_SHOW, SETTING_KEY.SCREEN_ALWAYS_ON, SETTING_KEY.NOTIFY_STYLE_CLASSIC, SETTING_KEY.IMMERSIVE_MODE, @@ -163,8 +159,6 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, arrayOf( binding.settingNavaigationSwitch, binding.settingShakeSwitch, - binding.settingLrcFloatSwitch, - binding.settingStatusbarLrcSwitch, binding.settingScreenSwitch, binding.settingNotifySwitch, binding.settingImmersiveSwitch, @@ -203,34 +197,6 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, } else { ShakeDetector.getInstance().stopListen() } - //桌面歌词 - R.id.setting_lrc_float_switch -> { - if (isChecked && !FloatWindowManager.getInstance().checkPermission(this@SettingActivity)) { - binding.settingLrcFloatSwitch.setOnCheckedChangeListener(null) - binding.settingLrcFloatSwitch.isChecked = false - binding.settingLrcFloatSwitch.setOnCheckedChangeListener(this) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) - intent.data = Uri.parse("package:$packageName") - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - Util.startActivitySafely(this@SettingActivity, intent) - } - ToastUtil.show(this@SettingActivity, R.string.plz_give_float_permission) - return - } - binding.settingLrcFloatTip.setText(if (isChecked) R.string.opened_desktop_lrc else R.string.closed_desktop_lrc) - val intent = MusicUtil.makeCmdIntent(Command.TOGGLE_DESKTOP_LYRIC) - intent.putExtra( - EXTRA_DESKTOP_LYRIC, binding.settingLrcFloatSwitch.isChecked - ) - sendLocalBroadcast(intent) - } - //状态栏歌词 - R.id.setting_statusbar_lrc_switch -> { - val intent = - MusicUtil.makeCmdIntent(Command.TOGGLE_STATUS_BAR_LRC) - sendLocalBroadcast(intent) - } //沉浸式状态栏 R.id.setting_immersive_switch -> { ThemeStore.sImmersiveMode = isChecked @@ -287,14 +253,30 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, view.setOnCheckedChangeListener(checkedChangedListener) } - //桌面歌词 - binding.settingLrcFloatTip.setText( - if (binding.settingLrcFloatSwitch.isChecked) R.string.opened_desktop_lrc else R.string.closed_desktop_lrc - ) - - if (!isSupportStatusBarLyric(this)) { + // 桌面歌词 & 状态栏歌词 + listOf(binding.settingLrcFloatSwitch, binding.settingStatusbarLrcSwitch).forEach { + TintHelper.setTintAuto(it, ThemeStore.accentColor, false) + } + if (LyricsManager.isDesktopLyricsEnabled) { + binding.settingLrcFloatSwitch.isChecked = true + binding.settingLrcFloatTip.setText(R.string.opened_desktop_lrc) + } else { + binding.settingLrcFloatSwitch.isChecked = false + binding.settingLrcFloatTip.setText(R.string.closed_desktop_lrc) + } + binding.settingLrcFloatSwitch.setOnCheckedChangeListener(this) + if (isStatusBarLyricsSupported(this)) { + binding.settingStatusbarLrcSwitch.run { + isChecked = LyricsManager.isStatusBarLyricsEnabled + setOnCheckedChangeListener(this@SettingActivity) + } + } else { binding.settingStatusbarLrcContainer.visibility = View.GONE } + getSharedPreferences( + LYRICS_KEY.NAME, + Context.MODE_PRIVATE + ).registerOnSharedPreferenceChangeListener(this) //主题颜色指示器 (binding.settingColorPrimaryIndicator.drawable as GradientDrawable).setColor( @@ -402,12 +384,6 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, binding.settingUpdateContainer.visibility = View.GONE } - getSharedPreferences( - SETTING_KEY.NAME, Context.MODE_PRIVATE - ).registerOnSharedPreferenceChangeListener( - this - ) - // 点击事件处理 arrayOf( binding.settingBlacklistContainer, @@ -914,10 +890,8 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, * 歌词搜索优先级 */ private fun configLyricPriority() { - ViewCommon.showLocalLyricTip(this) { - LyricPriorityDialog.newInstance().show( - supportFragmentManager, "configLyricPriority" - ) + LyricsHelper.showLocalLyricsTip(this) { + LyricsOrderDialog().show(supportFragmentManager, "configLyricPriority") } } @@ -1031,7 +1005,6 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, //清除配置文件、数据库等缓存 Util.deleteFilesByDirectory(cacheDir) // Util.deleteFilesByDirectory(externalCacheDir) - DiskCache.init(this@SettingActivity, "lyric") //清除glide缓存 Glide.get(this@SettingActivity).clearDiskCache() UriFetcher.clearAllCache() @@ -1312,15 +1285,38 @@ class SettingActivity : ToolbarActivity(), ColorChooserDialog.ColorCallback, } } + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + when (buttonView) { + binding.settingLrcFloatSwitch -> { + LyricsManager.setDesktopLyricsEnabled(isChecked, this) + binding.settingLrcFloatSwitch.run { + setOnCheckedChangeListener(null) + setChecked(LyricsManager.isDesktopLyricsEnabled) + setOnCheckedChangeListener(this@SettingActivity) + } + } + binding.settingStatusbarLrcSwitch -> LyricsManager.isStatusBarLyricsEnabled = isChecked + } + } + override fun onSharedPreferenceChanged( - sharedPreferences: SharedPreferences?, key: String? + sharedPreferences: SharedPreferences, key: String? ) { - if (key == SETTING_KEY.DESKTOP_LYRIC_SHOW) { - binding.settingLrcFloatSwitch.setOnCheckedChangeListener(null) - binding.settingLrcFloatSwitch.isChecked = SPUtil.getValue( - this, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_SHOW, false - ) - binding.settingLrcFloatSwitch.setOnCheckedChangeListener(checkedChangedListener) + when (key) { + LYRICS_KEY.DESKTOP_LYRICS_ENABLED -> { + binding.settingLrcFloatSwitch.run { + setOnCheckedChangeListener(null) + isChecked = LyricsManager.isDesktopLyricsEnabled + setOnCheckedChangeListener(this@SettingActivity) + } + } + LYRICS_KEY.STATUS_BAR_LYRICS_ENABLED -> { + binding.settingStatusbarLrcSwitch.run { + setOnCheckedChangeListener(null) + isChecked = LyricsManager.isStatusBarLyricsEnabled + setOnCheckedChangeListener(this@SettingActivity) + } + } } } diff --git a/app/src/main/java/remix/myplayer/ui/adapter/DesktopLyricColorAdapter.kt b/app/src/main/java/remix/myplayer/ui/adapter/DesktopLyricColorAdapter.kt deleted file mode 100644 index dccaab150..000000000 --- a/app/src/main/java/remix/myplayer/ui/adapter/DesktopLyricColorAdapter.kt +++ /dev/null @@ -1,102 +0,0 @@ -package remix.myplayer.ui.adapter - -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.GradientDrawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.RelativeLayout -import androidx.recyclerview.widget.RecyclerView -import remix.myplayer.R -import remix.myplayer.databinding.ItemFloatLrcColorBinding -import remix.myplayer.theme.GradientDrawableMaker -import remix.myplayer.theme.ThemeStore.floatLyricTextColor -import remix.myplayer.ui.adapter.DesktopLyricColorAdapter.FloatColorHolder -import remix.myplayer.ui.adapter.holder.BaseViewHolder -import remix.myplayer.util.ColorUtil -import remix.myplayer.util.DensityUtil - -/** - * Created by Remix on 2017/8/15. - */ -class DesktopLyricColorAdapter(context: Context?, layoutId: Int, width: Int) : BaseAdapter(layoutId) { - //当前桌面歌词的字体颜色 默认为当前主题颜色 - private var currentColor: Int - private var itemWidth: Int - - /** - * 判断是否是选中的颜色 - */ - private fun isColorChoose(context: Context, colorRes: Int): Boolean { - return context.resources.getColor(colorRes) == currentColor - } - - fun setCurrentColor(color: Int) { - currentColor = color - floatLyricTextColor = color - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FloatColorHolder { - val context = parent.context - val holder = FloatColorHolder( - LayoutInflater.from(context).inflate(R.layout.item_float_lrc_color, parent, false)) - val imgLayoutParam = RelativeLayout.LayoutParams( - DensityUtil.dip2px(context, 18f), DensityUtil.dip2px(context, 18f)) - imgLayoutParam.addRule(RelativeLayout.CENTER_IN_PARENT) - holder.binding.itemColor.layoutParams = imgLayoutParam - val rootLayoutParam = RecyclerView.LayoutParams(itemWidth, - ViewGroup.LayoutParams.MATCH_PARENT) - holder.binding.root.layoutParams = rootLayoutParam - return holder - } - - override fun convert(holder: FloatColorHolder, colorRes: Int?, position: Int) { - if (colorRes == null) { - return - } - val color = if (colorRes != R.color.md_white_primary) ColorUtil.getColor(colorRes) else Color.parseColor("#F9F9F9") - if (isColorChoose(holder.itemView.context, colorRes)) { - holder.binding.itemColor.background = GradientDrawableMaker() - .shape(GradientDrawable.OVAL) - .color(color) - .strokeSize(DensityUtil.dip2px(1f)) - .strokeColor(Color.BLACK) - .width(SIZE) - .height(SIZE) - .make() - } else { - holder.binding.itemColor.background = GradientDrawableMaker() - .shape(GradientDrawable.OVAL) - .color(color) - .width(SIZE) - .height(SIZE) - .make() - } - holder.binding.root.setOnClickListener { v: View? -> onItemClickListener?.onItemClick(v, position) } - } - - class FloatColorHolder(view: View) : BaseViewHolder(view) { - val binding: ItemFloatLrcColorBinding = ItemFloatLrcColorBinding.bind(view) - } - - companion object { - private val SIZE = DensityUtil.dip2px(18f) - val COLORS = listOf( - R.color.md_red_primary, R.color.md_brown_primary, R.color.md_navy_primary, - R.color.md_green_primary, R.color.md_yellow_primary, R.color.md_purple_primary, - R.color.md_indigo_primary, R.color.md_plum_primary, R.color.md_blue_primary, - R.color.md_white_primary, R.color.md_pink_primary - ) - } - - init { - setDataList(COLORS) - itemWidth = width / COLORS.size - //宽度太小 - if (itemWidth < DensityUtil.dip2px(context, 20f)) { - itemWidth = DensityUtil.dip2px(context, 20f) - } - currentColor = floatLyricTextColor - } -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/ui/adapter/LyricPriorityAdapter.kt b/app/src/main/java/remix/myplayer/ui/adapter/LyricPriorityAdapter.kt deleted file mode 100644 index 0d606edb7..000000000 --- a/app/src/main/java/remix/myplayer/ui/adapter/LyricPriorityAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package remix.myplayer.ui.adapter - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import remix.myplayer.R -import remix.myplayer.bean.misc.LyricPriority -import remix.myplayer.ui.adapter.holder.BaseViewHolder -import remix.myplayer.util.SPUtil - - -class LyricPriorityAdapter(context: Context?, layoutId: Int) : BaseAdapter(layoutId) { - - init { - val temp: ArrayList = Gson().fromJson(SPUtil.getValue(context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.PRIORITY_LYRIC, SPUtil.LYRIC_KEY.DEFAULT_PRIORITY), - object : TypeToken>() {}.type) - - val all = listOf( - LyricPriority.EMBEDDED, - LyricPriority.LOCAL, - LyricPriority.KUGOU, - LyricPriority.NETEASE, - LyricPriority.QQ, - LyricPriority.IGNORE) - if (temp.size < all.size) { - if (!temp.contains(LyricPriority.QQ)) { - temp.add(2,LyricPriority.QQ) - } - if (!temp.contains(LyricPriority.IGNORE)) { - temp.add(temp.size, LyricPriority.IGNORE) - } - - } - - setDataList(temp) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LyricPriorityHolder { - return LyricPriorityHolder(LayoutInflater.from(parent.context).inflate(layoutId, parent, false)) - } - - override fun convert(holder: LyricPriorityHolder, lyricPriority: LyricPriority?, position: Int) { - if(lyricPriority == null){ - return - } - - holder.view.findViewById(R.id.item_title)?.text = lyricPriority.desc - holder.view.setOnClickListener { - - } - } - - class LyricPriorityHolder(val view: View) : BaseViewHolder(view) -} diff --git a/app/src/main/java/remix/myplayer/ui/adapter/LyricsOrderAdapter.kt b/app/src/main/java/remix/myplayer/ui/adapter/LyricsOrderAdapter.kt new file mode 100644 index 000000000..e9a3c6e5c --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/adapter/LyricsOrderAdapter.kt @@ -0,0 +1,29 @@ +package remix.myplayer.ui.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import remix.myplayer.R +import remix.myplayer.lyrics.provider.ILyricsProvider + +// TODO: implements BaseAdapter? +class LyricsOrderAdapter(val order: List) : + RecyclerView.Adapter() { + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val title: TextView = view.findViewById(R.id.item_title) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + LayoutInflater.from(parent.context).inflate(R.layout.item_lyrics_order, parent, false).let { + return ViewHolder(it) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.title.text = order[position].displayName + } + + override fun getItemCount() = order.size +} diff --git a/app/src/main/java/remix/myplayer/ui/dialog/LyricPriorityDialog.kt b/app/src/main/java/remix/myplayer/ui/dialog/LyricPriorityDialog.kt deleted file mode 100644 index 2b264ce9a..000000000 --- a/app/src/main/java/remix/myplayer/ui/dialog/LyricPriorityDialog.kt +++ /dev/null @@ -1,79 +0,0 @@ -package remix.myplayer.ui.dialog - -import android.app.Dialog -import android.os.Bundle -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.ItemTouchHelper -import com.google.gson.Gson -import remix.myplayer.App -import remix.myplayer.R -import remix.myplayer.misc.cache.DiskCache -import remix.myplayer.theme.Theme -import remix.myplayer.ui.adapter.LyricPriorityAdapter -import remix.myplayer.ui.dialog.base.BaseMusicDialog -import remix.myplayer.util.SPUtil -import remix.myplayer.util.ToastUtil -import java.util.* - -class LyricPriorityDialog : BaseMusicDialog() { - companion object { - @JvmStatic - fun newInstance(): LyricPriorityDialog { - return LyricPriorityDialog() - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val view = requireActivity().layoutInflater.inflate(R.layout.dialog_lyric_priority, null) - - val adapter = LyricPriorityAdapter(context, R.layout.item_lyric_priority) - val recyclerView = view.findViewById(R.id.recycler_view) - recyclerView.layoutManager = LinearLayoutManager(activity) - ItemTouchHelper(object : ItemTouchHelper.Callback() { - override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { - val dragFlag = ItemTouchHelper.LEFT or ItemTouchHelper.DOWN or ItemTouchHelper.UP or ItemTouchHelper.RIGHT - return makeMovementFlags(dragFlag, 0) - } - - override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { - Collections.swap(adapter.dataList, if (viewHolder.adapterPosition >= 0) viewHolder.adapterPosition else 0, - if (target.adapterPosition >= 0) target.adapterPosition else 0) - adapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition) - return true - } - - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - - } - }).attachToRecyclerView(recyclerView) - - recyclerView.adapter = adapter - - return Theme.getBaseDialog(activity) - .title(R.string.lrc_priority) - .customView(view, false) - .positiveText(R.string.confirm) - .negativeText(R.string.cancel) - .onPositive { dialog, which -> - try { - DiskCache.getLrcDiskCache().delete() - DiskCache.init(App.context, "lyric") - SPUtil.deleteFile(App.context, SPUtil.LYRIC_KEY.NAME) - SPUtil.putValue(App.context, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.LYRIC_RESET_ON_16000, true) - SPUtil.putValue(activity, SPUtil.LYRIC_KEY.NAME, SPUtil.LYRIC_KEY.PRIORITY_LYRIC, - Gson().toJson(adapter.dataList)) - ToastUtil.show(context, R.string.save_success) - } catch (e: Exception) { - ToastUtil.show(context, R.string.save_error_arg, e.message) - } - - } - .onNegative { dialog, which -> - dismiss() - } - .build() - } - - -} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/ui/dialog/LyricsOrderDialog.kt b/app/src/main/java/remix/myplayer/ui/dialog/LyricsOrderDialog.kt new file mode 100644 index 000000000..a1b58ac13 --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/dialog/LyricsOrderDialog.kt @@ -0,0 +1,49 @@ +package remix.myplayer.ui.dialog + +import android.app.Dialog +import android.os.Bundle +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import remix.myplayer.R +import remix.myplayer.lyrics.LyricsSearcher +import remix.myplayer.theme.Theme +import remix.myplayer.ui.adapter.LyricsOrderAdapter +import remix.myplayer.ui.dialog.base.BaseDialog +import java.util.Collections + +class LyricsOrderDialog : BaseDialog() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + val recyclerView = RecyclerView(context) + val adapter = LyricsOrderAdapter(LyricsSearcher.order) + recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.adapter = adapter + + ItemTouchHelper(object : + ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val p1 = viewHolder.adapterPosition.takeIf { it in adapter.order.indices } ?: return false + val p2 = target.adapterPosition.takeIf { it in adapter.order.indices } ?: return false + Collections.swap(adapter.order, p1, p2) + adapter.notifyItemMoved(p1, p2) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} + }).attachToRecyclerView(recyclerView) + + return Theme.getBaseDialog(context) + .title(R.string.lrc_priority) + .customView(recyclerView, false) + .positiveText(R.string.confirm) + .negativeText(R.string.cancel) + .onPositive { _, _ -> LyricsSearcher.order = adapter.order } + .onNegative { _, _ -> dismiss() } + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/remix/myplayer/ui/fragment/LyricFragment.kt b/app/src/main/java/remix/myplayer/ui/fragment/LyricFragment.kt deleted file mode 100644 index db7c31385..000000000 --- a/app/src/main/java/remix/myplayer/ui/fragment/LyricFragment.kt +++ /dev/null @@ -1,223 +0,0 @@ -package remix.myplayer.ui.fragment - -import android.graphics.Color -import android.net.Uri -import android.os.Bundle -import android.os.Message -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import io.reactivex.disposables.Disposable -import io.reactivex.functions.Consumer -import remix.myplayer.App -import remix.myplayer.R -import remix.myplayer.bean.mp3.Song -import remix.myplayer.databinding.FragmentLrcBinding -import remix.myplayer.helper.MusicServiceRemote -import remix.myplayer.lyric.LyricSearcher -import remix.myplayer.misc.handler.MsgHandler -import remix.myplayer.misc.handler.OnHandleMessage -import remix.myplayer.misc.interfaces.OnInflateFinishListener -import remix.myplayer.theme.ThemeStore -import remix.myplayer.ui.fragment.base.BaseMusicFragment -import remix.myplayer.util.SPUtil -import remix.myplayer.util.ToastUtil -import timber.log.Timber -import java.util.* -import kotlin.math.abs - -/** - * Created by Remix on 2015/12/2. - */ - -/** - * 歌词界面Fragment - */ -class LyricFragment : BaseMusicFragment(), View.OnClickListener { - override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentLrcBinding - get() = FragmentLrcBinding::inflate - - private var onFindListener: OnInflateFinishListener? = null - private var song: Song? = null - - private var disposable: Disposable? = null - private val msgHandler = MsgHandler(this) - - private val lyricSearcher = LyricSearcher() - - fun setOnInflateFinishListener(l: OnInflateFinishListener) { - onFindListener = l - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - pageName = LyricFragment::class.java.simpleName - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.offsetReduce.setOnClickListener(this) - binding.offsetAdd.setOnClickListener(this) - binding.offsetReset.setOnClickListener(this) - onFindListener?.onViewInflateFinish(binding.lrcView) - //黑色主题着色按钮 - val themeRes = ThemeStore.themeRes - if (themeRes == R.style.Theme_APlayer_Black || themeRes == R.style.Theme_APlayer_Dark) { - binding.ivOffsetReduceArrow.setColorFilter(Color.WHITE) - binding.ivOffsetReduceSecond.setColorFilter(Color.WHITE) - binding.offsetReset.setColorFilter(Color.WHITE) - binding.ivOffsetAddArrow.setColorFilter(Color.WHITE) - binding.ivOffsetAddSecond.setColorFilter(Color.WHITE) - } - } - - override fun onDestroyView() { - msgHandler.remove() - disposable?.dispose() - onFindListener = null - super.onDestroyView() - } - - @JvmOverloads - fun updateLrc(song: Song, clearCache: Boolean = false) { - this.song = song - getLrc(Uri.EMPTY, clearCache) - } - - fun updateLrc(uri: Uri) { - getLrc(uri, true) - } - - private fun getLrc(uri: Uri, clearCache: Boolean) { - if (!isVisible) - return - if (song == null) { - binding.lrcView.setText(getStringSafely(R.string.no_lrc)) - return - } - if (clearCache) { - //清除offset - SPUtil.putValue(App.context, SPUtil.LYRIC_OFFSET_KEY.NAME, song?.id.toString() + "", 0) - binding.lrcView.setOffset(0) - } - val id = song?.id - - disposable?.dispose() - binding.lrcView.setText(getStringSafely(R.string.searching)) - disposable = lyricSearcher.setSong(song ?: return) - .getLyricObservable(uri, clearCache) - .subscribe(Consumer { - Timber.v("setLrcRows") - if (id == song?.id) { - if (it == null || it.isEmpty()) { - binding.lrcView.setText(getStringSafely(R.string.no_lrc)) - return@Consumer - } - binding.lrcView.setOffset( - SPUtil.getValue( - requireContext(), - SPUtil.LYRIC_OFFSET_KEY.NAME, - song?.id.toString() + "", - 0 - ) - ) - binding.lrcView.setLrcRows(it) - } - }, Consumer { - Timber.v(it) - if (id == song?.id) { - binding.lrcView.setLrcRows(null) - binding.lrcView.setText(getStringSafely(R.string.no_lrc)) - } - }) - } - - override fun onClick(view: View) { - msgHandler.removeMessages(MESSAGE_HIDE) - msgHandler.sendEmptyMessageDelayed(MESSAGE_HIDE, DELAY_HIDE) - - val originalOffset = - SPUtil.getValue(requireContext(), SPUtil.LYRIC_OFFSET_KEY.NAME, song?.id.toString() + "", 0) - var newOffset = originalOffset - when (view.id) { - R.id.offsetReset -> { - newOffset = 0 - ToastUtil.show(requireContext(), R.string.lyric_offset_reset) - } - R.id.offsetAdd -> newOffset += 500 - R.id.offsetReduce -> newOffset -= 500 - } - if (originalOffset != newOffset) { - SPUtil.putValue( - requireContext(), - SPUtil.LYRIC_OFFSET_KEY.NAME, - song?.id.toString() + "", - newOffset - ) - val toastMsg = msgHandler.obtainMessage(MESSAGE_SHOW_TOAST) - toastMsg.arg1 = newOffset - msgHandler.removeMessages(MESSAGE_SHOW_TOAST) - msgHandler.sendMessageDelayed(toastMsg, DELAY_SHOW_TOAST) - binding.lrcView.setOffset(newOffset) - MusicServiceRemote.setLyricOffset(newOffset) - } - - } - - fun showLyricOffsetView() { - if (binding.lrcView.getLrcRows().isNullOrEmpty()) { - ToastUtil.show(requireContext(), R.string.no_lrc) - return - } - binding.offsetContainer.visibility = View.VISIBLE - msgHandler.sendEmptyMessageDelayed(MESSAGE_HIDE, DELAY_HIDE) - } - - fun setLyricScalingFactor(choose: Int) { - val factor = when (choose) { - 0 -> { - 1f - } - 1 -> { - 1.5f - } - 2 -> { - 2f - } - else -> { - 1f - } - } - binding.lrcView.setLrcScalingFactor(factor) - } - - @OnHandleMessage - fun handleInternal(msg: Message) { - when (msg.what) { - MESSAGE_HIDE -> { - binding.offsetContainer.visibility = View.GONE - } - MESSAGE_SHOW_TOAST -> { - val newOffset = msg.arg1 - if (newOffset != 0 && abs(newOffset) <= 60000) {//最大偏移60s - ToastUtil.show( - requireContext(), - if (newOffset > 0) R.string.lyric_advance_x_second else R.string.lyric_delay_x_second, - String.format(Locale.getDefault(), "%.1f", newOffset / 1000f) - ) - } - } - } - } - - - companion object { - private const val DELAY_HIDE = 5000L - private const val DELAY_SHOW_TOAST = 100L - - private const val MESSAGE_HIDE = 1 - private const val MESSAGE_SHOW_TOAST = 2 - } - -} diff --git a/app/src/main/java/remix/myplayer/ui/fragment/LyricsFragment.kt b/app/src/main/java/remix/myplayer/ui/fragment/LyricsFragment.kt new file mode 100644 index 000000000..69074e487 --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/fragment/LyricsFragment.kt @@ -0,0 +1,139 @@ +package remix.myplayer.ui.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.annotation.UiThread +import androidx.core.view.isVisible +import remix.myplayer.R +import remix.myplayer.databinding.FragmentLrcBinding +import remix.myplayer.lyrics.LyricsLine +import remix.myplayer.lyrics.LyricsManager +import remix.myplayer.ui.fragment.base.BaseMusicFragment +import remix.myplayer.ui.widget.LyricsView +import timber.log.Timber +import kotlin.math.sign +import kotlin.time.Duration.Companion.milliseconds + +class LyricsFragment : BaseMusicFragment(), View.OnClickListener { + override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> FragmentLrcBinding + get() = FragmentLrcBinding::inflate + + companion object { + private const val TAG = "LyricsFragment" + + private val HIDE_PANEL_DELAY = 5000.milliseconds + } + + init { + pageName = TAG + } + + var onSeekToListener: LyricsView.OnSeekToListener? = null + + @UiThread + fun setLyricsSearching() { + view ?: return + binding.offsetPanel.visibility = View.GONE + binding.lyrics.visibility = View.GONE + binding.lyricsNoLrc.visibility = View.GONE + binding.lyricsSearching.visibility = View.VISIBLE + } + + @UiThread + fun setLyrics(lyrics: List) { + view ?: return + binding.offsetPanel.visibility = View.GONE + binding.lyricsSearching.visibility = View.GONE + if (lyrics.isEmpty()) { + binding.lyrics.visibility = View.GONE + binding.lyricsNoLrc.visibility = View.VISIBLE + } else { + binding.lyricsNoLrc.visibility = View.GONE + binding.lyrics.visibility = View.VISIBLE + binding.lyrics.lyrics = lyrics + } + } + + private fun hasLyrics(): Boolean { + return binding.lyrics.isVisible + } + + @UiThread + fun setOffset(offset: Long) { + view ?: return + if (hasLyrics()) { + binding.lyrics.offset = offset + } + } + + @UiThread + fun setProgress(progress: Long, duration: Long) { + view ?: return + if (hasLyrics()) { + binding.lyrics.setProgress(progress, duration) + } + } + + private val hideOffsetPanelRunnable = Runnable { + binding.offsetPanel.visibility = View.GONE + } + private var toast: Toast? = null + + override fun onClick(v: View) { + if (!hasLyrics()) { + Timber.tag(TAG).w("Trying to set offset when no lyrics") + return + } + when (v.id) { + R.id.offset_inc -> LyricsManager.offset += 500 + R.id.offset_dec -> LyricsManager.offset -= 500 + R.id.offset_reset -> LyricsManager.offset = 0 + else -> check(false) + } + val message = when (LyricsManager.offset.sign) { + +1 -> resources.getString(R.string.lyric_advance_x_second, LyricsManager.offset / 1000.0) + -1 -> resources.getString(R.string.lyric_delay_x_second, LyricsManager.offset / 1000.0) + 0 -> resources.getString(R.string.lyric_offset_reset) + else -> { + Timber.tag(TAG).wtf("offset sign??") + return + } + } + // 取消上一次的通知,及时显示最新的 + // TODO: ToastUtil ? + toast?.cancel() + toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast!!.show() + binding.root.handler.removeCallbacks(hideOffsetPanelRunnable) + binding.root.handler.postDelayed(hideOffsetPanelRunnable, HIDE_PANEL_DELAY.inWholeMilliseconds) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.offsetInc.setOnClickListener(this) + binding.offsetDec.setOnClickListener(this) + binding.offsetReset.setOnClickListener(this) + binding.lyrics.onSeekToListener = LyricsView.OnSeekToListener { + onSeekToListener?.onSeekTo(it) + } + LyricsManager.setLyricsFragment(this) + } + + @UiThread + fun showOffsetPanel() { + if (hasLyrics()) { + binding.offsetPanel.visibility = View.VISIBLE + binding.root.handler.run { + removeCallbacks(hideOffsetPanelRunnable) + postDelayed(hideOffsetPanelRunnable, HIDE_PANEL_DELAY.inWholeMilliseconds) + } + } else { + // 搜索中 / 没有歌词 不应显示偏移设置 + // TODO: maybe toast? + } + } +} diff --git a/app/src/main/java/remix/myplayer/ui/misc/PartialForegroundColorSpan.kt b/app/src/main/java/remix/myplayer/ui/misc/PartialForegroundColorSpan.kt new file mode 100644 index 000000000..b4036e554 --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/misc/PartialForegroundColorSpan.kt @@ -0,0 +1,60 @@ +package remix.myplayer.ui.misc + +import android.graphics.Canvas +import android.graphics.Paint +import android.text.style.ReplacementSpan +import androidx.annotation.ColorInt +import androidx.core.graphics.withClip +import kotlin.math.roundToInt + +/** + * 从左边开始对文字的一部分使用特定颜色的 Span + * + * @param proportion 0 到 1 之间的值,表示特定颜色部分的宽度占比 + * + * TODO: RTL? + */ +class PartialForegroundColorSpan( + private val proportion: Double, @ColorInt private val color: Int +) : ReplacementSpan() { + init { + require(proportion in 0.0..1.0) + } + + override fun getSize( + paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt? + ): Int { + if (fm != null) { + paint.getFontMetricsInt(fm) + } + return paint.measureText(text, start, end).roundToInt() + } + + override fun draw( + canvas: Canvas, + text: CharSequence, start: Int, end: Int, + x: Float, top: Int, y: Int, bottom: Int, + paint: Paint + ) { + val width = paint.measureText(text, start, end) + + if (proportion == 0.0) { + canvas.drawText(text, start, end, x, y.toFloat(), paint) + return + } + if (proportion == 1.0) { + paint.color = color + canvas.drawText(text, start, end, x, y.toFloat(), paint) + return + } + + val mid = (x + width * proportion).toFloat() + canvas.withClip(mid, top.toFloat(), x + width, bottom.toFloat()) { + drawText(text, start, end, x, y.toFloat(), paint) + } + paint.color = color + canvas.withClip(x, top.toFloat(), mid, bottom.toFloat()) { + drawText(text, start, end, x, y.toFloat(), paint) + } + } +} diff --git a/app/src/main/java/remix/myplayer/ui/widget/LyricsView.kt b/app/src/main/java/remix/myplayer/ui/widget/LyricsView.kt new file mode 100644 index 000000000..c58a43701 --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/widget/LyricsView.kt @@ -0,0 +1,284 @@ +package remix.myplayer.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.UiThread +import remix.myplayer.databinding.LayoutLyricsLineBinding +import remix.myplayer.databinding.LayoutLyricsViewBinding +import remix.myplayer.lyrics.LyricsLine +import remix.myplayer.lyrics.PerWordLyricsLine +import remix.myplayer.theme.ThemeStore +import timber.log.Timber +import kotlin.math.roundToInt +import kotlin.math.roundToLong +import kotlin.time.Duration.Companion.milliseconds + +class LyricsView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs), View.OnTouchListener { + companion object { + private const val TAG = "LyricsView" + + private val DEACTIVATE_DELAY = 5000.milliseconds + private val AUTO_SCROLL_DELAY = 200.milliseconds + + private val normalTextColor + @ColorInt get() = ThemeStore.textColorSecondary + private val highlightTextColor + @ColorInt get() = ThemeStore.textColorPrimary + } + + fun interface OnSeekToListener { + fun onSeekTo(progress: Long) + } + + private val binding = LayoutLyricsViewBinding.inflate(LayoutInflater.from(context), this, true) + + var onSeekToListener: OnSeekToListener? = null + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + Timber.tag(TAG).v("onSizeChanged, h=$h") + if (h != oldh) { + // 给 container 上下加空白,确保第一行和最后一行歌词可以滚动到 view 中间 + val padding = (h + 1) / 2 + handler.post { + binding.innerContainer.setPadding(0, padding, 0, padding) + handler.post { // 要等一次 layout + if (!isActive) { + scrollToLine(lastHighlightLine) + } + } + } + } + } + + private fun addLayoutForLine(line: LyricsLine) { + val layout = + LayoutLyricsLineBinding.inflate(LayoutInflater.from(context), binding.innerContainer, true) + if (line.content.isNotBlank()) { + layout.content.text = if (line is PerWordLyricsLine) { + line.getSpannedString(0.0, normalTextColor) + } else { + line.content + } + } + if (!line.translation.isNullOrBlank()) { + layout.translation.text = line.translation + } else { + layout.translation.visibility = View.GONE + } + } + + /** + * 修改完后应立刻设置 offset + */ + var lyrics: List = emptyList() + @UiThread set(value) { + field = value + binding.innerContainer.removeAllViews() + isClickable = value.isNotEmpty() + value.forEach { + addLayoutForLine(it) + } + rawProgressAndDuration = null + lastHighlightLine = null + isActive = false + } + + private data class ProgressAndDuration(val progress: Long, val duration: Long) + private var rawProgressAndDuration: ProgressAndDuration? = null + + /** + * 修改时自动更新 UI + */ + var offset: Long = 0 + @UiThread set(value) { + if (value == field) { + return + } + field = value + if (isActive) { + updateTimeIndicator() + } + rawProgressAndDuration?.run { + setProgress(progress, duration) + } + } + + private fun getTextViewOfLine(index: Int): TextView { + val layout = binding.innerContainer.getChildAt(index) as LinearLayout + return layout.getChildAt(0) as TextView + } + + private fun setProgressOfLine(index: Int, progress: Double, @ColorInt color: Int) { + val view = getTextViewOfLine(index) + val line = lyrics[index] + if (line.content.isBlank()) { + return + } + if (line is PerWordLyricsLine) { + view.text = line.getSpannedString(progress, color) + } else { + view.setTextColor(color) + } + } + + private var lastHighlightLine: Int? = null + + @UiThread + fun setProgress(rawProgress: Long, rawDuration: Long) { + check(lyrics.isNotEmpty()) + rawProgressAndDuration = ProgressAndDuration(rawProgress, rawDuration) + val progress = rawProgress + offset + val duration = rawDuration + offset + check(progress <= duration) + val index = lyrics.binarySearchBy(progress) { it.time }.let { + if (it < 0) -(it + 1) - 1 else it + } + check(index >= -1 && index < lyrics.size) + if (index != lastHighlightLine) { + lastHighlightLine?.let { + setProgressOfLine(it, 0.0, normalTextColor) + lastHighlightLine = null + } + if (!isActive) { + scrollToLine(index) + } + } + if (index >= 0) { + val line = lyrics[index] + setProgressOfLine( + index, if (line is PerWordLyricsLine) { + line.getProgress( + progress, lyrics.getOrNull(index + 1)?.time ?: duration + ) + } else 0.0, highlightTextColor + ) + lastHighlightLine = index + } + } + + private fun getNearestLine(): Int { + val y = binding.outerContainer.scrollY + binding.outerContainer.height / 2f + var line: Int = -1 + var minDistance = Float.POSITIVE_INFINITY + for (i in lyrics.indices) { + val view = binding.innerContainer.getChildAt(i) + if (y >= view.top && y <= view.bottom) { + return i + } + val distance = if (y < view.top) (view.top - y) else (y - view.bottom) + if (distance < minDistance) { + line = i + minDistance = distance + } + } + check(line != -1) + return line + } + + private fun scrollToLine(line: Int?) { + val y = if (line == null || line < 0) { + 0 + } else { + val view = binding.innerContainer.getChildAt(line) + ((view.top + view.bottom - binding.outerContainer.height) / 2f).roundToInt() + } + binding.outerContainer.smoothScrollTo(0, y) + } + + private var isActive: Boolean = false + set(value) { + field = value + if (value) { + updateTimeIndicator() + binding.timeIndicator.visibility = View.VISIBLE + handler.removeCallbacks(deactivateRunnable) + handler.postDelayed(deactivateRunnable, DEACTIVATE_DELAY.inWholeMilliseconds) + } else { + binding.timeIndicator.visibility = View.GONE + rawProgressAndDuration?.run { + setProgress(progress, duration) + } + } + } + + private val deactivateRunnable = Runnable { + isActive = false + scrollToLine(lastHighlightLine) + } + private val scrollToNearestLineRunnable = Runnable { + scrollToLine(getNearestLine()) + } + + @SuppressLint("SetTextI18n") + private fun updateTimeIndicator() { + (lyrics[getNearestLine()].time - offset).coerceIn(0, rawProgressAndDuration?.duration ?: 0).let { + val time = (it / 10.0).roundToLong() + binding.time.text = "%02d:%02d.%02d".format(time / 100 / 60, time / 100 % 60, time % 100) + binding.playButton.setOnClickListener { _ -> + onSeekToListener?.onSeekTo(it) + } + } + } + + private var isTouching: Boolean = false + + override fun onTouch(v: View, event: MotionEvent): Boolean { + check(v == binding.outerContainer) + + isActive = true + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isTouching = true + handler.removeCallbacks(scrollToNearestLineRunnable) + } + + MotionEvent.ACTION_UP -> { + isTouching = false + handler.postDelayed(scrollToNearestLineRunnable, AUTO_SCROLL_DELAY.inWholeMilliseconds) + } + } + + return false + } + + private fun onScrollChange() { + updateTimeIndicator() + + handler.removeCallbacks(scrollToNearestLineRunnable) + if (!isTouching) { + handler.postDelayed(scrollToNearestLineRunnable, AUTO_SCROLL_DELAY.inWholeMilliseconds) + } + } + + // 在单独函数以忽略警告 + @SuppressLint("ClickableViewAccessibility") + private fun init() { + binding.outerContainer.setOnTouchListener(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + binding.outerContainer.setOnScrollChangeListener { _, _, _, _, _ -> + onScrollChange() + } + } else { + binding.outerContainer.viewTreeObserver.addOnScrollChangedListener { + onScrollChange() + } + } + } + + init { + init() + } +} diff --git a/app/src/main/java/remix/myplayer/ui/widget/SingleLineLyricsView.kt b/app/src/main/java/remix/myplayer/ui/widget/SingleLineLyricsView.kt new file mode 100644 index 000000000..08639abdb --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/widget/SingleLineLyricsView.kt @@ -0,0 +1,167 @@ +package remix.myplayer.ui.widget + +import android.content.Context +import android.graphics.Canvas +import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.UiThread +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.graphics.withClip +import remix.myplayer.lyrics.LyricsLine +import remix.myplayer.lyrics.PerWordLyricsLine +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * 显示单行歌词(不换行)的 View + * + * + * TODO: RTL支持? + */ +class SingleLineLyricsView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyleAttr) { + companion object { + private const val ELLIPSIS = Typography.ellipsis.toString() + } + + @ColorInt var sungColor: Int = currentTextColor + @UiThread set(@ColorInt value) { + if (value != field) { + field = value + invalidate() + } + } + + @ColorInt var unsungColor: Int = currentTextColor + @UiThread set(@ColorInt value) { + if (value != field) { + field = value + invalidate() + } + } + + /** + * 整行的全部文字 + */ + private var content = "" + var lyricsLine: LyricsLine? = null + @UiThread set(value) { + if (value == field) { + return + } + field = value + // 不能在这里就把 content 换成省略号,不然后面判断会出问题 + content = lyricsLine?.content ?: "" + contentDescription = content + if (progress != null) { + progress = null + // 已间接调用 invalidate() + } else { + invalidate() + } + requestLayout() + } + + /** + * 当前进度 + * + * 取值范围: + * - `lyricsLine` is `PerWordLyricsLine`:[0, lyricsLine.words.size] + * - 否则:[0, 1] + */ + var progress: Double? = null + @UiThread set(value) { + if (value == field) { + return + } + if (value != null) { + lyricsLine.let { + check(it != null) + require(value in 0.0..(if (it is PerWordLyricsLine) it.words.size.toDouble() else 1.0)) + } + } + field = value + invalidate() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + + val width = if (widthMode == MeasureSpec.EXACTLY) { + widthSize + } else { + val desired = paint.measureText(content.ifBlank { ELLIPSIS }).roundToInt() + if (widthMode == MeasureSpec.AT_MOST) min(desired, widthSize) else desired + } + val height = if (heightMode == MeasureSpec.EXACTLY) { + heightSize + } else { + val fm = paint.fontMetrics + val desired = (fm.bottom - fm.top).roundToInt() + if (heightMode == MeasureSpec.AT_MOST) min(desired, heightSize) else desired + } + setMeasuredDimension(width, height) + } + + override fun onDraw(canvas: Canvas) { + // Make compiler happy + val content = content + val lyricsLine = lyricsLine + val offset = progress + + val textWidth = paint.measureText(content.ifBlank { ELLIPSIS }) + val fm = paint.fontMetrics + paint.color = sungColor + if (content.isBlank() || lyricsLine == null || offset == null) { + // 进度未设置时居中显示 + // 无可用内容时显示省略号 + canvas.drawText(content.ifBlank { ELLIPSIS }, (width - textWidth) / 2, -fm.top, paint) + } else { + if (lyricsLine is PerWordLyricsLine) { + val index = offset.toInt() + if (index < lyricsLine.words.size) { + val l = + paint.measureText(lyricsLine.words.subList(0, index).joinToString("") { it.content }) + val r = paint.measureText( + lyricsLine.words.subList(0, index + 1).joinToString("") { it.content }) + val highlightWidth = l + (r - l) * (offset - index) + val left = (if (width >= textWidth) { + (width - textWidth) / 2 + } else { + (width / 2.0 - highlightWidth).coerceIn((width - textWidth).toDouble(), 0.0) + }).toFloat() + val mid = (left + highlightWidth).toFloat() + canvas.withClip(left, 0f, mid, height.toFloat()) { + drawText(content, left, -fm.top, paint) + } + paint.color = unsungColor + canvas.withClip(mid, 0f, left + textWidth, height.toFloat()) { + drawText(content, left, -fm.top, paint) + } + } else { + check(index == lyricsLine.words.size) + val left = if (width >= textWidth) { + (width - textWidth) / 2 + } else { + width - textWidth + } + canvas.drawText(content, left, -fm.top, paint) + } + } else { + val left = if (width >= textWidth) { + (width - textWidth) / 2 + } else { + (width - textWidth) * offset + } + canvas.drawText(content, left.toFloat(), -fm.top, paint) + } + } + } +} diff --git a/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricTextView.java b/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricTextView.java deleted file mode 100644 index b61f0e302..000000000 --- a/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricTextView.java +++ /dev/null @@ -1,146 +0,0 @@ -package remix.myplayer.ui.widget.desktop; - -import android.animation.ValueAnimator; -import android.content.Context; -import android.content.res.ColorStateList; -import android.graphics.Canvas; -import android.graphics.Rect; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import android.text.TextPaint; -import android.util.AttributeSet; -import remix.myplayer.lyric.bean.LrcRow; - -/** - * @ClassName - * @Description - * @Author Xiaoborui - * @Date 2017/5/11 14:22 - */ - -public class DesktopLyricTextView extends androidx.appcompat.widget.AppCompatTextView { - - private static final int DELAY_MAX = 100; - - /** - * 当前x坐标 - */ - private float mCurTextXForHighLightLrc; - /** - * 当前的歌词 - */ - private LrcRow mCurLrcRow; - /** - * 当前歌词的字符串所占的控件 - */ - private Rect mTextRect = new Rect(); - /** - * 垂直便宜 - */ - private int mOffsetY; - /*** - * 监听属性动画的数值值的改变 - */ - ValueAnimator.AnimatorUpdateListener mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { - - @Override - public void onAnimationUpdate(ValueAnimator animation) { - mCurTextXForHighLightLrc = (Float) animation.getAnimatedValue(); - invalidate(); - } - }; - - public DesktopLyricTextView(Context context) { - this(context, null); - } - - public DesktopLyricTextView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public DesktopLyricTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - private void init() { - } - - /** - * 控制歌词水平滚动的属性动画 - ***/ - private ValueAnimator mAnimator; - - /** - * 开始水平滚动歌词 - * - * @param endX 歌词第一个字的最终的x坐标 - * @param duration 滚动的持续时间 - */ - private void startScrollLrc(float endX, long duration) { - if (mAnimator == null) { - mAnimator = ValueAnimator.ofFloat(0, endX); - mAnimator.addUpdateListener(mUpdateListener); - } else { - mCurTextXForHighLightLrc = 0; - mAnimator.cancel(); - mAnimator.setFloatValues(0, endX); - } - mAnimator.setDuration(duration); - long delay = (long) (duration * 0.1); - mAnimator.setStartDelay(delay > DELAY_MAX ? DELAY_MAX : delay); //延迟执行属性动画 - mAnimator.start(); - } - - @Override - public void setTextColor(ColorStateList colors) { - super.setTextColor(colors); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mCurLrcRow == null) { - return; - } - canvas.drawText(mCurLrcRow.getContent(), - mCurTextXForHighLightLrc, - (getHeight() - getPaint().getFontMetrics().top - getPaint().getFontMetrics().bottom) / 2, - getPaint()); - } - - public void setLrcRow(@NonNull LrcRow lrcRow) { - if (lrcRow.getTime() != 0 && mCurLrcRow != null && mCurLrcRow.getTime() == lrcRow.getTime()) { - return; - } - mCurLrcRow = lrcRow; - stopAnimation(); -// setText(mCurLrcRow.getContent()); - - TextPaint paint = getPaint(); - if (paint == null) { - return; - } - String text = mCurLrcRow.getContent(); - paint.getTextBounds(text, 0, text.length(), mTextRect); - float textWidth = mTextRect.width(); - mOffsetY = (int) ((mTextRect.bottom + mTextRect.top - paint.getFontMetrics().bottom - paint - .getFontMetrics().top) / 2); - if (textWidth > getWidth()) { - //如果歌词宽度大于view的宽,则需要动态设置歌词的起始x坐标,以实现水平滚动 - startScrollLrc(getWidth() - textWidth, (long) (mCurLrcRow.getTotalTime() * 0.85)); - } else { - //如果歌词宽度小于view的宽,则让歌词居中显示 - mCurTextXForHighLightLrc = (getWidth() - textWidth) / 2; - invalidate(); - } - - } - - public void stopAnimation() { - if (mAnimator != null && mAnimator.isRunning()) { - mAnimator.cancel(); - } - invalidate(); - } -} diff --git a/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricView.kt b/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricView.kt deleted file mode 100644 index 697c00ef8..000000000 --- a/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricView.kt +++ /dev/null @@ -1,520 +0,0 @@ -package remix.myplayer.ui.widget.desktop - -import android.app.Service -import android.content.Context -import android.graphics.Color -import android.graphics.PointF -import android.hardware.input.InputManager -import android.os.Build -import android.os.Message -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewTreeObserver -import android.view.WindowManager -import android.widget.RelativeLayout -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener -import androidx.recyclerview.widget.LinearLayoutManager -import com.afollestad.materialdialogs.internal.MDTintHelper -import remix.myplayer.R -import remix.myplayer.databinding.LayoutDesktopLyricBinding -import remix.myplayer.lyric.bean.LrcRow -import remix.myplayer.misc.handler.MsgHandler -import remix.myplayer.misc.handler.OnHandleMessage -import remix.myplayer.misc.interfaces.OnItemClickListener -import remix.myplayer.service.Command -import remix.myplayer.service.MusicService -import remix.myplayer.service.MusicService.Companion.EXTRA_DESKTOP_LYRIC -import remix.myplayer.theme.ThemeStore -import remix.myplayer.ui.adapter.DesktopLyricColorAdapter -import remix.myplayer.ui.adapter.DesktopLyricColorAdapter.Companion.COLORS -import remix.myplayer.util.* -import remix.myplayer.util.MusicUtil.makeCmdIntent -import remix.myplayer.util.SPUtil.SETTING_KEY -import remix.myplayer.util.Util.sendLocalBroadcast -import timber.log.Timber -import kotlin.math.abs - - -/** - * @ClassName - * @Description - * @Author Xiaoborui - * @Date 2017/3/22 15:47 - */ - -class DesktopLyricView(private val service: MusicService) : RelativeLayout(service), View.OnClickListener { - private val binding: LayoutDesktopLyricBinding = - LayoutDesktopLyricBinding.inflate(LayoutInflater.from(service), this, true) - - private val windowManager: WindowManager by lazy { - service.getSystemService(Context.WINDOW_SERVICE) as WindowManager - } - private val lastPoint = PointF() - var isLocked = false - private set - private val handler = MsgHandler(this) - private val colorAdapter: DesktopLyricColorAdapter by lazy { - DesktopLyricColorAdapter( - service, - R.layout.item_float_lrc_color, - binding.widgetColorRecyclerview.measuredWidth - ) - } - - private var textSizeType = MEDIUM - private val hideRunnable = Runnable { - binding.widgetPanel.visibility = View.GONE - binding.widgetLrcContainer.visibility = View.GONE - } - - private val onSeekBarChangeListener = object : OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { - val temp = Color.rgb( - binding.widgetSeekbarR.progress, - binding.widgetSeekbarG.progress, - binding.widgetSeekbarB.progress - ) - val color = if (ColorUtil.isColorCloseToWhite(temp)) Color.parseColor("#F9F9F9") else temp - binding.widgetLine1.setTextColor(color) - MDTintHelper.setTint(binding.widgetSeekbarR, color) - MDTintHelper.setTint(binding.widgetSeekbarG, color) - MDTintHelper.setTint(binding.widgetSeekbarB, color) - binding.widgetTextR.setTextColor(color) - binding.widgetTextG.setTextColor(color) - binding.widgetTextB.setTextColor(color) - resetHide() - - handler.removeMessages(MESSAGE_SAVE_COLOR) - handler.sendMessageDelayed(Message.obtain(handler, MESSAGE_SAVE_COLOR, color, 0), 100) - } - - override fun onStartTrackingTouch(seekBar: SeekBar) { - - } - - override fun onStopTrackingTouch(seekBar: SeekBar) { - - } - } - - /** - * 当前是否正在拖动 - */ - private var isDragging = false - - init { - setUpView() - } - - private fun setUpColor() { - binding.widgetColorRecyclerview.viewTreeObserver - .addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - binding.widgetColorRecyclerview.viewTreeObserver.removeOnPreDrawListener(this) - colorAdapter.onItemClickListener = object : OnItemClickListener { - override fun onItemClick(view: View, position: Int) { - val color = ColorUtil.getColor(COLORS[position]) - binding.widgetLine1.setTextColor(color) - colorAdapter.setCurrentColor(color) - colorAdapter.notifyDataSetChanged() - resetHide() - } - - override fun onItemLongClick(view: View, position: Int) { - - } - } - binding.widgetColorRecyclerview.layoutManager = LinearLayoutManager( - service, - LinearLayoutManager.HORIZONTAL, - false - ) - binding.widgetColorRecyclerview.overScrollMode = View.OVER_SCROLL_NEVER - binding.widgetColorRecyclerview.adapter = colorAdapter - return true - } - }) - } - - private fun setUpView() { - val temp = ThemeStore.floatLyricTextColor - val color = if (ColorUtil.isColorCloseToWhite(temp)) Color.parseColor("#F9F9F9") else temp - val red = color and 0xff0000 shr 16 - val green = color and 0x00ff00 shr 8 - val blue = color and 0x0000ff - binding.widgetSeekbarR.max = 255 - binding.widgetSeekbarR.progress = red - binding.widgetSeekbarG.max = 255 - binding.widgetSeekbarG.progress = green - binding.widgetSeekbarB.max = 255 - binding.widgetSeekbarB.progress = blue - binding.widgetTextR.setTextColor(color) - binding.widgetTextG.setTextColor(color) - binding.widgetTextB.setTextColor(color) - binding.widgetSeekbarR.setOnSeekBarChangeListener(onSeekBarChangeListener) - binding.widgetSeekbarG.setOnSeekBarChangeListener(onSeekBarChangeListener) - binding.widgetSeekbarB.setOnSeekBarChangeListener(onSeekBarChangeListener) - MDTintHelper.setTint(binding.widgetSeekbarR, color) - MDTintHelper.setTint(binding.widgetSeekbarG, color) - MDTintHelper.setTint(binding.widgetSeekbarB, color) - - textSizeType = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_TEXT_SIZE, MEDIUM) - binding.widgetLine1.setTextColor(color) - binding.widgetLine1.textSize = getTextSize(TYPE_TEXT_SIZE_FIRST_LINE) - binding.widgetLine2.textSize = getTextSize(TYPE_TEXT_SIZE_SECOND_LINE) - isLocked = SPUtil.getValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_LOCK, false) - - setPlayIcon(service.isPlaying) - - viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { - override fun onPreDraw(): Boolean { - viewTreeObserver.removeOnPreDrawListener(this) - saveLock(isLocked, false) - return true - } - }) - - arrayOf( - binding.widgetClose, - binding.widgetLock, - binding.widgetNext, - binding.widgetPlay, - binding.widgetPrev, - binding.widgetLrcBigger, - binding.widgetLrcSmaller, - binding.widgetSetting - ).forEach { - it.setOnClickListener(this) - } - } - - /** - * @param type 0: 第一行 1:第二行 - */ - private fun getTextSize(type: Int): Float { - return when (type) { - TYPE_TEXT_SIZE_FIRST_LINE -> { - (when (textSizeType) { - TINY -> FIRST_LINE_TINY - SMALL -> FIRST_LINE_SMALL - MEDIUM -> FIRST_LINE_MEDIUM - BIG -> FIRST_LINE_BIG - else -> FIRST_LINE_HUGE - }).toFloat() - } - TYPE_TEXT_SIZE_SECOND_LINE -> { - (when (textSizeType) { - TINY -> SECOND_LINE_TINY - SMALL -> SECOND_LINE_SMALL - MEDIUM -> SECOND_LINE_MEDIUM - BIG -> SECOND_LINE_BIG - else -> SECOND_LINE_HUGE - }).toFloat() - } - else -> throw IllegalArgumentException("unknown textSize type") - } - } - - fun setText(lrc1: LrcRow?, lrc2: LrcRow?) { - if (lrc1 != null) { - if (TextUtils.isEmpty(lrc1.content)) { - lrc1.content = "......" - } - binding.widgetLine1.setLrcRow(lrc1) - } - if (lrc2 != null) { - if (TextUtils.isEmpty(lrc2.content)) { - lrc2.content = "....." - } - binding.widgetLine2.text = lrc2.content - } - } - - override fun onTouchEvent(event: MotionEvent): Boolean { - when (event.action) { - MotionEvent.ACTION_DOWN -> if (!isLocked) { - isDragging = false - lastPoint.set(event.rawX, event.rawY) - handler.removeCallbacks(hideRunnable) - } else { -// mUIHandler.postDelayed(mLongClickRunnable, LONGCLICK_THRESHOLD); - } - MotionEvent.ACTION_MOVE -> if (!isLocked) { - val params = layoutParams as WindowManager.LayoutParams - - if (abs(event.rawY - lastPoint.y) > DISTANCE_THRESHOLD) { - params.y += (event.rawY - lastPoint.y).toInt() - isDragging = true - if (isAttachedToWindow) { - windowManager.updateViewLayout(this, params) - } - } - lastPoint.set(event.rawX, event.rawY) - } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> if (!isLocked) { - if (!isDragging) { - //点击后隐藏或者显示操作栏 - if (binding.widgetPanel.isShown) { - binding.widgetPanel.visibility = View.INVISIBLE - } else { - binding.widgetPanel.visibility = View.VISIBLE - handler.postDelayed(hideRunnable, DISMISS_THRESHOLD.toLong()) - } - } else { - //滑动 - if (binding.widgetPanel.isShown) { - handler.postDelayed(hideRunnable, DISMISS_THRESHOLD.toLong()) - } - isDragging = false - } - //保存y坐标 - val params = layoutParams as WindowManager.LayoutParams - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_Y, params.y) - } else { -// mUIHandler.removeCallbacks(mLongClickRunnable) - } - } - return true - } - - fun setPlayIcon(play: Boolean) { - binding.widgetPlay.setImageResource( - if (play) R.drawable.widget_btn_stop_normal - else R.drawable.widget_btn_play_normal - ) - } - - - override fun onClick(view: View) { - when (view.id) { - //关闭桌面歌词 - R.id.widget_close -> { - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_SHOW, - false) - sendLocalBroadcast( - makeCmdIntent(Command.TOGGLE_DESKTOP_LYRIC).putExtra(EXTRA_DESKTOP_LYRIC, false)) - } - //锁定 - R.id.widget_lock -> { - saveLock(lock = true, toast = true) - handler.postDelayed(hideRunnable, 0) - Util.sendCMDLocalBroadcast(Command.LOCK_DESKTOP_LYRIC) - } - //歌词字体、大小设置 - R.id.widget_setting -> { - binding.widgetLrcContainer.visibility = if (binding.widgetLrcContainer.isShown) View.GONE else View.VISIBLE - setUpColor() - //操作后重置消息的时间 - resetHide() - } - R.id.widget_next, R.id.widget_play, R.id.widget_prev -> { - sendLocalBroadcast(makeCmdIntent( - when (view.id) { - R.id.widget_next -> Command.NEXT - R.id.widget_prev -> Command.PREV - else -> Command.TOGGLE - } - )) - handler.postDelayed({ - binding.widgetPlay.setImageResource( - if (service.isPlaying) - R.drawable.widget_btn_stop_normal - else - R.drawable.widget_btn_play_normal) - }, 100) - //操作后重置消息的时间 - resetHide() - } - //字体放大、缩小 - R.id.widget_lrc_bigger, R.id.widget_lrc_smaller -> { - var needRefresh = false - if (view.id == R.id.widget_lrc_bigger) { - //当前已经是最大字体 - if (textSizeType == HUGE) { - return - } - textSizeType++ - needRefresh = true - } - if (view.id == R.id.widget_lrc_smaller) { - //当前已经是最小字体 - if (textSizeType == TINY) { - return - } - textSizeType-- - needRefresh = true - } - if (needRefresh) { - binding.widgetLine1.textSize = getTextSize(TYPE_TEXT_SIZE_FIRST_LINE) - binding.widgetLine2.textSize = getTextSize(TYPE_TEXT_SIZE_SECOND_LINE) - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_TEXT_SIZE, textSizeType) - //操作后重置消息的时间 - resetHide() - } - } - } - } - - fun saveLock(lock: Boolean, toast: Boolean) { - isLocked = lock - SPUtil.putValue(service, SETTING_KEY.NAME, SETTING_KEY.DESKTOP_LYRIC_LOCK, isLocked) - if (toast) { - ToastUtil.show(service, if (!isLocked) R.string.desktop_lyric__unlock else R.string.desktop_lyric_lock) - } - val params = layoutParams as WindowManager.LayoutParams? - if (params != null) { - if (lock) { - //锁定后点击通知栏解锁 - // mNotify.notifyToUnlock(); - params.flags = (WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val inputManager = context.getSystemService(Service.INPUT_SERVICE) as InputManager - params.alpha = inputManager.maximumObscuringOpacityForTouch - } - } else { - // mNotify.cancel(); - params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - } - if (isAttachedToWindow) { - windowManager.updateViewLayout(this, params) - } - } - } - - /** - * 应用退出后清除通知 - */ - // public void cancelNotify() { - // Timber.v("取消解锁通知"); - // if (mNotify != null) { - // mNotify.cancel(); - // } - // } - - /** - * 操作后重置消失的时间 - */ - private fun resetHide() { - handler.removeCallbacks(hideRunnable) - handler.postDelayed(hideRunnable, DISMISS_THRESHOLD.toLong()) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - handler.removeCallbacksAndMessages(null) - binding.widgetSeekbarR.setOnSeekBarChangeListener(null) - binding.widgetSeekbarG.setOnSeekBarChangeListener(null) - binding.widgetSeekbarB.setOnSeekBarChangeListener(null) - Timber.v("onDetachedFromWindow") - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - Timber.v("onAttachedToWindow") - } - - @OnHandleMessage - fun handleMsg(msg: Message) { - when (msg.what) { - MESSAGE_SAVE_COLOR -> ThemeStore.floatLyricTextColor = msg.arg1 - } - } - - companion object { - - // private UnLockNotify mNotify; - - //当前字体大小 - private const val TINY = 0 - private const val SMALL = 1 - private const val MEDIUM = 2 - private const val BIG = 3 - private const val HUGE = 4 - - //第一行歌词字体大小 - private const val FIRST_LINE_HUGE = 23 - private const val FIRST_LINE_BIG = 20 - private const val FIRST_LINE_MEDIUM = 18 - private const val FIRST_LINE_SMALL = 17 - private const val FIRST_LINE_TINY = 16 - - //第二行歌词字体大小 - private const val SECOND_LINE_HUGE = 20 - private const val SECOND_LINE_BIG = 18 - private const val SECOND_LINE_MEDIUM = 16 - private const val SECOND_LINE_SMALL = 15 - private const val SECOND_LINE_TINY = 14 - - private const val TYPE_TEXT_SIZE_FIRST_LINE = 0 - private const val TYPE_TEXT_SIZE_SECOND_LINE = 1 - - private const val DISTANCE_THRESHOLD = 10 - private const val DISMISS_THRESHOLD = 4500 - private const val LONG_CLICK_THRESHOLD = 1000 - - - private const val MESSAGE_SAVE_COLOR = 1 - } - - // private static class UnLockNotify { - // - // private static final String UNLOCK_NOTIFICATION_CHANNEL_ID = "unlock_notification"; - // private static final int UNLOCK_NOTIFICATION_ID = 2; - // private Context mContext; - // private NotificationManager mNotificationManager; - // - // UnLockNotify() { - // mContext = App.getContext(); - // mNotificationManager = (NotificationManager) mContext - // .getSystemService(Context.NOTIFICATION_SERVICE); - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // initNotificationChanel(); - // } - // } - // - // @RequiresApi(api = Build.VERSION_CODES.O) - // private void initNotificationChanel() { - // NotificationChannel notificationChannel = new NotificationChannel( - // UNLOCK_NOTIFICATION_CHANNEL_ID, mContext.getString(R.string.unlock_notification), - // NotificationManager.IMPORTANCE_LOW); - // notificationChannel.setShowBadge(false); - // notificationChannel.enableLights(false); - // notificationChannel.enableVibration(false); - // notificationChannel - // .setDescription(mContext.getString(R.string.unlock_notification_description)); - // mNotificationManager.createNotificationChannel(notificationChannel); - // } - // - // void notifyToUnlock() { - // Notification notification = new NotificationCompat.Builder(mContext, - // UNLOCK_NOTIFICATION_CHANNEL_ID) - // .setContentText(mContext.getString(R.string.desktop_lyric_lock)) - // .setContentTitle(mContext.getString(R.string.click_to_unlock)) - // .setShowWhen(false) - // .setPriority(NotificationCompat.PRIORITY_DEFAULT) - // .setOngoing(true) - // .setTicker(mContext.getString(R.string.desktop_lyric__lock_ticker)) - // .setContentIntent(buildPendingIntent()) - // .setSmallIcon(R.drawable.icon_notifbar) - // .build(); - // mNotificationManager.notify(UNLOCK_NOTIFICATION_ID, notification); - // } - // - // void cancel() { - // mNotificationManager.cancel(UNLOCK_NOTIFICATION_ID); - // } - // - // PendingIntent buildPendingIntent() { - // Intent intent = new Intent(MusicService.ACTION_CMD); - // intent.putExtra("Control", Command.UNLOCK_DESKTOP_LYRIC); - // intent.setComponent(new ComponentName(mContext, MusicService.class)); - // return PendingIntent.getService(mContext, Command.UNLOCK_DESKTOP_LYRIC, intent, - // PendingIntent.FLAG_UPDATE_CURRENT); - // } - // } -} diff --git a/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricsView.kt b/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricsView.kt new file mode 100644 index 000000000..c49c3538b --- /dev/null +++ b/app/src/main/java/remix/myplayer/ui/widget/desktop/DesktopLyricsView.kt @@ -0,0 +1,407 @@ +package remix.myplayer.ui.widget.desktop + +import android.app.Service +import android.content.Context +import android.content.res.Configuration +import android.hardware.input.InputManager +import android.os.Build +import android.util.AttributeSet +import android.util.TypedValue.COMPLEX_UNIT_SP +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewConfiguration +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.annotation.ColorInt +import androidx.annotation.UiThread +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.children +import androidx.core.widget.ImageViewCompat +import remix.myplayer.R +import remix.myplayer.databinding.LayoutDesktopLyricsBinding +import remix.myplayer.helper.MusicServiceRemote +import remix.myplayer.lyrics.CurrentNextLyricsLine +import remix.myplayer.lyrics.LyricsManager +import remix.myplayer.service.Command +import remix.myplayer.theme.MaterialTintHelper +import remix.myplayer.util.MusicUtil.makeCmdIntent +import remix.myplayer.util.SPUtil +import remix.myplayer.util.SPUtil.DESKTOP_LYRICS_KEY +import remix.myplayer.util.ToastUtil +import remix.myplayer.util.Util.sendLocalBroadcast +import timber.log.Timber +import kotlin.math.abs +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds + +@UiThread +class DesktopLyricsView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null +) : FrameLayout(context, attrs) { + companion object { + private const val TAG = "DesktopLyricsView" + + private const val ELLIPSIS = Typography.ellipsis.toString() + + private val HIDE_PANEL_DELAY = 3000.milliseconds + + private const val DEFAULT_FIRST_LINE_SIZE = 18f + private const val DEFAULT_SECOND_LINE_SIZE = 16f + + @ColorInt + private const val DEFAULT_SUNG_COLOR = 0xff698cf6.toInt() + + @ColorInt + private const val DEFAULT_UNSUNG_COLOR = 0xffd4d4d4.toInt() + + @ColorInt + private const val DEFAULT_TRANSLATION_COLOR = 0xffd4d4d4.toInt() + } + + private val binding = LayoutDesktopLyricsBinding.inflate(LayoutInflater.from(context), this, true) + + private val windowManager by lazy { + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + } + private val inputManager by lazy { + context.getSystemService(Service.INPUT_SERVICE) as InputManager + } + + private var secondLineIsTranslation: Boolean = false + + private var yPosition: Int + get() = SPUtil.getValue( + context, + DESKTOP_LYRICS_KEY.NAME, + DESKTOP_LYRICS_KEY.Y_POSITION_PREFIX + resources.configuration.orientation, + 0 + ) + set(value) = SPUtil.putValue( + context, + DESKTOP_LYRICS_KEY.NAME, + DESKTOP_LYRICS_KEY.Y_POSITION_PREFIX + resources.configuration.orientation, + value + ) + private var firstLineSize: Float + get() = SPUtil.getValue( + context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.FIRST_LINE_SIZE, DEFAULT_FIRST_LINE_SIZE + ) + set(v) { + binding.linesContainer.firstLine.setTextSize(COMPLEX_UNIT_SP, v) + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.FIRST_LINE_SIZE, v) + // TODO("fix window position?") + } + private var secondLineSize: Float + get() = SPUtil.getValue( + context, + DESKTOP_LYRICS_KEY.NAME, + DESKTOP_LYRICS_KEY.SECOND_LINE_SIZE, + DEFAULT_SECOND_LINE_SIZE + ) + set(v) { + binding.linesContainer.secondLine.setTextSize(COMPLEX_UNIT_SP, v) + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.SECOND_LINE_SIZE, v) + // TODO("fix window position?") + } + + var sungColor: Int + @ColorInt get() = SPUtil.getValue( + context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.SUNG_COLOR, DEFAULT_SUNG_COLOR + ) + set(@ColorInt v) { + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.SUNG_COLOR, v) + binding.linesContainer.firstLine.sungColor = v + } + var unsungColor: Int + @ColorInt get() = SPUtil.getValue( + context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.UNSUNG_COLOR, DEFAULT_UNSUNG_COLOR + ) + set(@ColorInt v) { + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.UNSUNG_COLOR, v) + binding.linesContainer.firstLine.unsungColor = v + if (!secondLineIsTranslation) { + binding.linesContainer.secondLine.setTextColor(v) + } + } + var translationColor: Int + @ColorInt get() = SPUtil.getValue( + context, + DESKTOP_LYRICS_KEY.NAME, + DESKTOP_LYRICS_KEY.TRANSLATION_COLOR, + DEFAULT_TRANSLATION_COLOR + ) + set(@ColorInt v) { + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.TRANSLATION_COLOR, v) + if (secondLineIsTranslation) { + binding.linesContainer.secondLine.setTextColor(v) + } + } + + var isPlaying: Boolean = true // 初值与 xml 对应 + set(value) { + if (value != field) { + field = value + binding.playPause.setImageResource( + if (value) R.drawable.ic_pause_black_24dp + else R.drawable.ic_play_arrow_black_24dp + ) + ImageViewCompat.setImageTintList( + binding.playPause, + AppCompatResources.getColorStateList(context, R.color.desktop_lyrics_control_color) + ) + } + } + + fun setLyrics(content: CurrentNextLyricsLine) { + binding.linesContainer.firstLine.lyricsLine = content.currentLine + binding.linesContainer.firstLine.progress = content.currentLineProgress + if (!content.currentLine?.translation.isNullOrBlank()) { + setTranslation(content.currentLine?.translation!!) + } else { + // 翻译和下一行歌词都没有时显示省略号,统一用显示下一行歌词的颜色 + setNextLine((content.nextLine?.content ?: "").ifBlank { ELLIPSIS }) + } + } + + private fun setTranslation(translation: String) { + if (!secondLineIsTranslation) { + secondLineIsTranslation = true + binding.linesContainer.secondLine.setTextColor(translationColor) + } + if (translation != binding.linesContainer.secondLine.text) { + binding.linesContainer.secondLine.text = translation + } + } + + private fun setNextLine(content: String) { + if (secondLineIsTranslation) { + secondLineIsTranslation = false + binding.linesContainer.secondLine.setTextColor(unsungColor) + } + if (content != binding.linesContainer.secondLine.text) { + binding.linesContainer.secondLine.text = content + } + } + + private var isPanelVisible: Boolean = true // 初值与 xml 对应 + set(value) { + if (value != field) { + Timber.tag(TAG).v("set isPanelVisible: $value") + field = value + binding.root.setBackgroundColor( + ResourcesCompat.getColor( + resources, + if (value) R.color.desktop_lyrics_window_background else R.color.transparent, + null + ) + ) + binding.root.children.forEach { + if (it.id != R.id.lines_container) { + it.visibility = if (value) View.VISIBLE else View.GONE + } + } + if (value) { + // 控制组件由隐藏转为显示时,字体颜色和大小设置默认隐藏 + isSettingsVisible = false + } + } + if (value) { + handler.run { + removeCallbacks(hidePanelRunnable) + postDelayed(hidePanelRunnable, HIDE_PANEL_DELAY.inWholeMilliseconds) + } + } + } + + private val hidePanelRunnable = Runnable { + isPanelVisible = false + } + + private var isSettingsVisible: Boolean = true // 初值与 xml 对应 + set(value) { + arrayOf(binding.divider, binding.settingsContainer).forEach { + it.visibility = if (value) View.VISIBLE else View.GONE + } + field = value + } + + var isLocked: Boolean + get() = SPUtil.getValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.LOCKED, false) + set(value) { + if (isLocked != value) { + SPUtil.putValue(context, DESKTOP_LYRICS_KEY.NAME, DESKTOP_LYRICS_KEY.LOCKED, value) + ToastUtil.show( + context, if (value) R.string.desktop_lyric_lock else R.string.desktop_lyric__unlock + ) + } + (layoutParams as WindowManager.LayoutParams).run { + if (value) { + flags = flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + alpha = inputManager.maximumObscuringOpacityForTouch + } + } else { + flags = flags and WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE.inv() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + alpha = 1f + } + } + windowManager.updateViewLayout(this@DesktopLyricsView, this) + } + MusicServiceRemote.service?.run { + updateNotification() + updatePlaybackState() + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + binding.linesContainer.firstLine.setTextSize(COMPLEX_UNIT_SP, firstLineSize) + binding.linesContainer.secondLine.setTextSize(COMPLEX_UNIT_SP, secondLineSize) + binding.linesContainer.firstLine.sungColor = sungColor + binding.linesContainer.firstLine.unsungColor = unsungColor + + isLocked = isLocked + + isPanelVisible = false + + binding.close.setOnClickListener { + LyricsManager.setDesktopLyricsEnabled(false) + } + binding.prev.setOnClickListener { + sendLocalBroadcast(makeCmdIntent(Command.PREV)) + isPanelVisible = true + } + binding.playPause.setOnClickListener { + sendLocalBroadcast(makeCmdIntent(Command.TOGGLE)) + isPanelVisible = true + } + binding.next.setOnClickListener { + sendLocalBroadcast(makeCmdIntent(Command.NEXT)) + isPanelVisible = true + } + binding.lock.setOnClickListener { + isPanelVisible = false + isLocked = true + ToastUtil.show(context, R.string.desktop_lyric_lock) + } + binding.settings.setOnClickListener { + isSettingsVisible = !isSettingsVisible + isPanelVisible = true + } + binding.colorSettings.setOnClickListener { + handler?.removeCallbacks(hidePanelRunnable) + // TODO: Set colors + isPanelVisible = true // 设置完回来触发自动隐藏? + } + MaterialTintHelper.setTint(binding.firstLineSizeSlider) + MaterialTintHelper.setTint(binding.secondLineSizeSlider) + binding.firstLineSizeSlider.setLabelFormatter { "${it}sp" } + binding.secondLineSizeSlider.setLabelFormatter { "${it}sp" } + binding.firstLineSizeSlider.value = firstLineSize + binding.secondLineSizeSlider.value = secondLineSize + // TODO: Change on stop? onStopTrackingTouch + binding.firstLineSizeSlider.addOnChangeListener { _, value, _ -> + isPanelVisible = true + firstLineSize = value + } + binding.secondLineSizeSlider.addOnChangeListener { _, value, _ -> + isPanelVisible = true + secondLineSize = value + } + } + + private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop + private var isTouching = false + private var isDragging = false + private var lastPointerY: Float? = null + private var lastWindowY: Int? = null + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (isLocked) { + Timber.tag(TAG).w("isLocked is true but received touch event: $event") + return false + } + if (event.actionMasked != MotionEvent.ACTION_DOWN && !isTouching) { + // 会收到全屏幕的点击事件,但只有 ACTION_DOWN 在 View 上的是需要我们处理的 + return false + } + val params = layoutParams as WindowManager.LayoutParams + return when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { +// Timber.tag(TAG).d("onTouchEvent ACTION_DOWN ${event.y} $top $bottom $height") + if (event.y < top || event.y > bottom) { + isTouching = false + false + } else { + handler?.removeCallbacks(hidePanelRunnable) + isTouching = true + isDragging = false + lastPointerY = event.rawY + lastWindowY = params.y + true + } + } + + MotionEvent.ACTION_MOVE -> { + if (abs(event.rawY - lastPointerY!!) >= touchSlop) { + isDragging = true + } + if (isDragging) { + params.y = lastWindowY!! + (event.rawY - lastPointerY!!).roundToInt() + windowManager.updateViewLayout(this, params) + } + true + } + + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + isTouching = false + if (isDragging) { + params.y = lastWindowY!! + (event.rawY - lastPointerY!!).roundToInt() + windowManager.updateViewLayout(this, params) + isPanelVisible = isPanelVisible // 触发自动隐藏 + isDragging = false + lastPointerY = null + lastWindowY = null + saveWindowLocation() + } else { + if (event.action == MotionEvent.ACTION_UP) { + // 避免警告 + performClick() + } + } + true + } + + else -> false + } + } + + override fun performClick(): Boolean { + if (super.performClick()) { + return true + } + isPanelVisible = true + return true + } + + private fun saveWindowLocation() { + yPosition = (layoutParams as WindowManager.LayoutParams).y + } + + fun restoreWindowPosition() { + val params = layoutParams as WindowManager.LayoutParams + params.y = yPosition + windowManager.updateViewLayout(this, params) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + // TODO: Does it work? + Timber.tag(TAG).v("onConfigurationChanged, new orientation: ${newConfig.orientation}") + restoreWindowPosition() + } +} diff --git a/app/src/main/java/remix/myplayer/util/SPUtil.java b/app/src/main/java/remix/myplayer/util/SPUtil.java index e06e83807..d98c747e6 100644 --- a/app/src/main/java/remix/myplayer/util/SPUtil.java +++ b/app/src/main/java/remix/myplayer/util/SPUtil.java @@ -2,13 +2,8 @@ import android.content.Context; import android.content.SharedPreferences; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; -import remix.myplayer.bean.misc.LyricPriority; /** * Created by taeja on 16-1-15. @@ -54,6 +49,12 @@ public static void putValue(Context context, String name, String key, long value editor.putLong(key, value).apply(); } + public static void putValue(Context context, String name, String key, float value) { + SharedPreferences.Editor editor = context.getSharedPreferences(name, Context.MODE_PRIVATE) + .edit(); + editor.putFloat(key, value).apply(); + } + public static void putValue(Context context, String name, String key, String value) { SharedPreferences.Editor editor = context.getSharedPreferences(name, Context.MODE_PRIVATE) .edit(); @@ -79,6 +80,10 @@ public static long getValue(Context context, String name, Object key, long dft) return context.getSharedPreferences(name, Context.MODE_PRIVATE).getLong(key.toString(), dft); } + public static float getValue(Context context, String name, Object key, float dft) { + return context.getSharedPreferences(name, Context.MODE_PRIVATE).getFloat(key.toString(), dft); + } + public static String getValue(Context context, String name, Object key, String dft) { return context.getSharedPreferences(name, Context.MODE_PRIVATE).getString(key.toString(), dft); } @@ -102,34 +107,15 @@ public interface UPDATE_KEY { String IGNORE_FOREVER = "ignore_forever"; } - public interface LYRIC_KEY { - - String NAME = "Lyric"; - //歌词搜索优先级 - String PRIORITY_LYRIC = "priority_lyric"; - String DEFAULT_PRIORITY = new Gson().toJson(Arrays - .asList( - LyricPriority.EMBEDDED, - LyricPriority.LOCAL, - LyricPriority.KUGOU, - LyricPriority.NETEASE, - LyricPriority.QQ, - LyricPriority.IGNORE), - new TypeToken>() { - }.getType()); - - int LYRIC_DEFAULT = LyricPriority.DEF.getPriority(); - int LYRIC_IGNORE = LyricPriority.IGNORE.getPriority(); - int LYRIC_NETEASE = LyricPriority.NETEASE.getPriority(); - int LYRIC_KUGOU = LyricPriority.KUGOU.getPriority(); - int LYRIC_QQ = LyricPriority.QQ.getPriority(); - int LYRIC_LOCAL = LyricPriority.LOCAL.getPriority(); - int LYRIC_EMBEDDED = LyricPriority.EMBEDDED.getPriority(); - int LYRIC_MANUAL = LyricPriority.MANUAL.getPriority(); - - String LYRIC_FONT_SIZE = "lyric_font_size"; - String LYRIC_RESET_ON_16000 = "lyric_reset_on_16000"; - String LYRIC_LOCAL_TIP_SHOWN = "lyric_local_tip_shown"; + public interface LYRICS_KEY { + + String NAME = "Lyrics"; + + String DESKTOP_LYRICS_ENABLED = "desktop_lyrics_enabled"; + String STATUS_BAR_LYRICS_ENABLED = "status_bar_lyrics_enabled"; + String LOCAL_LYRICS_TIP_SHOWN = "local_lyrics_tip_shown"; + String ORDER = "order"; + String OFFSET_PREFIX = "offset_"; // offset_$hashKey } public interface COVER_KEY { @@ -137,17 +123,26 @@ public interface COVER_KEY { String NAME = "Cover"; } + public interface DESKTOP_LYRICS_KEY { + + String NAME = "DesktopLyrics"; + + // 以下所有设置项一般情况下应在 DesktopLyricsView 内部读/写 + + String LOCKED = "locked"; + String Y_POSITION_PREFIX = "y_position_"; // y_position_$orientation + String FIRST_LINE_SIZE = "first_line_size"; + String SECOND_LINE_SIZE = "second_line_size"; + String SUNG_COLOR = "sung_color"; + String UNSUNG_COLOR = "unsung_color"; + String TRANSLATION_COLOR = "translation_color"; + } + public interface SETTING_KEY { String NAME = "Setting"; //第一次读取数据 String FIRST_LOAD = "first_load"; - //桌面歌词是否可移动 - String DESKTOP_LYRIC_LOCK = "desktop_lyric_lock"; - //桌面歌词字体大小 - String DESKTOP_LYRIC_TEXT_SIZE = "desktop_lyric_text_size"; - //桌面歌词y坐标 - String DESKTOP_LYRIC_Y = "desktop_lyric_y"; //是否开启屏幕常亮 String SCREEN_ALWAYS_ON = "key_screen_always_on"; //通知栏是否启用经典样式 @@ -164,12 +159,6 @@ public interface SETTING_KEY { String COLOR_NAVIGATION = "color_Navigation"; //摇一摇 String SHAKE = "shake"; - //优先搜索在线歌词 - String ONLINE_LYRIC_FIRST = "online_lyric_first"; - //是否开启桌面歌词 - String DESKTOP_LYRIC_SHOW = "desktop_lyric_show"; - //是否开启状态栏歌词 - String STATUSBAR_LYRIC_SHOW = "statusbar_lyric_show"; //沉浸式状态栏 String IMMERSIVE_MODE = "immersive_mode"; //过滤大小 @@ -278,11 +267,6 @@ public interface SETTING_KEY { int NEWEST_VERSION = 3; } - public interface LYRIC_OFFSET_KEY { - - String NAME = "LyricOffset"; - } - public interface OTHER_KEY { String NAME = "Other"; diff --git a/app/src/main/java/remix/myplayer/util/SystemPropertiesUtil.kt b/app/src/main/java/remix/myplayer/util/SystemPropertiesUtil.kt new file mode 100644 index 000000000..38c239b7c --- /dev/null +++ b/app/src/main/java/remix/myplayer/util/SystemPropertiesUtil.kt @@ -0,0 +1,20 @@ +package remix.myplayer.util + +import android.annotation.SuppressLint +import timber.log.Timber + +object SystemPropertiesUtil { + private const val TAG = "SystemPropertiesUtil" + + @SuppressLint("PrivateApi") + private val clazz = Class.forName("android.os.SystemProperties") + + fun get(key: String): String { + try { + return clazz.getDeclaredMethod("get", String::class.java).invoke(null, key) as String + } catch (t: Throwable) { + Timber.tag(TAG).e(t, "Failed to invoke get") + return "" + } + } +} diff --git a/app/src/main/java/remix/myplayer/util/Util.kt b/app/src/main/java/remix/myplayer/util/Util.kt index 2bb274a38..456baa668 100644 --- a/app/src/main/java/remix/myplayer/util/Util.kt +++ b/app/src/main/java/remix/myplayer/util/Util.kt @@ -31,7 +31,6 @@ import remix.myplayer.App import remix.myplayer.App.Companion.context import remix.myplayer.R import remix.myplayer.bean.mp3.Song -import remix.myplayer.misc.floatpermission.rom.RomUtils import remix.myplayer.misc.manager.APlayerActivityManager import timber.log.Timber import java.io.* @@ -543,12 +542,16 @@ object Util { /** * 判断是否支持状态栏歌词 */ - fun isSupportStatusBarLyric(context: Context): Boolean { - return RomUtils.checkIsMeizuRom() || Settings.System.getInt( + fun isStatusBarLyricsSupported(context: Context): Boolean { + return Settings.System.getInt( context.contentResolver, "status_bar_show_lyric", 0 - ) != 0 || RomUtils.checkIsbaolong24Rom() || RomUtils.checkIsexTHmUIRom() + ) != 0 || Build.DISPLAY.contains( + "flyme", + true + ) || SystemPropertiesUtil.get("org.baolong24.device") + .isNotEmpty() || SystemPropertiesUtil.get("ro.exthm.device").isNotEmpty() } /** diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset.png deleted file mode 100644 index 420af6ef4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset_second.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset_second.png deleted file mode 100644 index a006dbcdf..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_add_offset_second.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset.png deleted file mode 100644 index 667f620a4..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset_second.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset_second.png deleted file mode 100644 index 8cae8618e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_reduce_offset_second.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_reset.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_reset.png deleted file mode 100644 index c533e9e3d..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_reset.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/icon_lyric_timeline.png b/app/src/main/res/drawable-xxhdpi/icon_lyric_timeline.png deleted file mode 100644 index 8bb98807e..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/icon_lyric_timeline.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset.png b/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset.png deleted file mode 100644 index 524c766c0..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset_second.png b/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset_second.png deleted file mode 100644 index 5d66f5636..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_lyric_add_offset_second.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset.png b/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset.png deleted file mode 100644 index 9ff56133a..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset_second.png b/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset_second.png deleted file mode 100644 index 22f6da991..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reduce_offset_second.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reset.png b/app/src/main/res/drawable-xxxhdpi/icon_lyric_reset.png deleted file mode 100644 index 3277d74d8..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/icon_lyric_reset.png and /dev/null differ diff --git a/app/src/main/res/drawable/ic_drag_handle_24dp.xml b/app/src/main/res/drawable/ic_drag_handle_24dp.xml new file mode 100644 index 000000000..c1280dad6 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_handle_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_lock_24dp.xml b/app/src/main/res/drawable/ic_lock_24dp.xml new file mode 100644 index 000000000..88f09039d --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_looks_one_24dp.xml b/app/src/main/res/drawable/ic_looks_one_24dp.xml new file mode 100644 index 000000000..2297a90dd --- /dev/null +++ b/app/src/main/res/drawable/ic_looks_one_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_looks_two_24dp.xml b/app/src/main/res/drawable/ic_looks_two_24dp.xml new file mode 100644 index 000000000..6d820bb82 --- /dev/null +++ b/app/src/main/res/drawable/ic_looks_two_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_palette_24dp.xml b/app/src/main/res/drawable/ic_palette_24dp.xml new file mode 100644 index 000000000..0de8857eb --- /dev/null +++ b/app/src/main/res/drawable/ic_palette_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_refresh_24dp.xml b/app/src/main/res/drawable/ic_refresh_24dp.xml new file mode 100644 index 000000000..a1e4f05de --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_24dp.xml b/app/src/main/res/drawable/ic_settings_24dp.xml new file mode 100644 index 000000000..2b1946bfb --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_stat_1_24dp.xml b/app/src/main/res/drawable/ic_stat_1_24dp.xml new file mode 100644 index 000000000..73a6c7a6a --- /dev/null +++ b/app/src/main/res/drawable/ic_stat_1_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_stat_minus_1_24dp.xml b/app/src/main/res/drawable/ic_stat_minus_1_24dp.xml new file mode 100644 index 000000000..379401170 --- /dev/null +++ b/app/src/main/res/drawable/ic_stat_minus_1_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_text_decrease_24dp.xml b/app/src/main/res/drawable/ic_text_decrease_24dp.xml new file mode 100644 index 000000000..f800f35f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_text_decrease_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_text_increase_24dp.xml b/app/src/main/res/drawable/ic_text_increase_24dp.xml new file mode 100644 index 000000000..3ad11f16f --- /dev/null +++ b/app/src/main/res/drawable/ic_text_increase_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_desktop_lyrics_color_settings.xml b/app/src/main/res/layout/dialog_desktop_lyrics_color_settings.xml new file mode 100644 index 000000000..9c3dd4613 --- /dev/null +++ b/app/src/main/res/layout/dialog_desktop_lyrics_color_settings.xml @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_lrc.xml b/app/src/main/res/layout/fragment_lrc.xml index 8570987f1..8e7f91a27 100644 --- a/app/src/main/res/layout/fragment_lrc.xml +++ b/app/src/main/res/layout/fragment_lrc.xml @@ -1,83 +1,79 @@ + android:padding="16dp" + android:orientation="vertical"> - + android:visibility="gone" /> + + + + + - - - + android:visibility="gone" + tools:visibility="visible"> - - +