├── .github └── workflows │ └── android.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── xposed_init │ ├── java │ ├── cn │ │ └── zhaiyifan │ │ │ └── lyric │ │ │ ├── Constants.java │ │ │ ├── LyricUtils.java │ │ │ └── model │ │ │ └── Lyric.java │ ├── com │ │ └── android │ │ │ └── settingslib │ │ │ ├── utils │ │ │ └── BuildCompatUtils.java │ │ │ └── widget │ │ │ ├── MainSwitchBar.java │ │ │ ├── MainSwitchPreference.java │ │ │ └── OnMainSwitchChangeListener.java │ └── statusbar │ │ └── finder │ │ ├── CSLyricHelper.java │ │ ├── LrcGetter.kt │ │ ├── app │ │ ├── App.kt │ │ ├── LyricsActivity.kt │ │ ├── LyricsAdapter.kt │ │ ├── MusicListenerService.java │ │ ├── SettingsActivity.kt │ │ ├── broadcast │ │ │ └── LyricsBroadcastReceiver.kt │ │ └── event │ │ │ ├── AppsListChanged.kt │ │ │ ├── LyricSentenceUpdate.kt │ │ │ ├── LyricsChange.kt │ │ │ └── LyricsResultChange.kt │ │ ├── config │ │ └── Config.kt │ │ ├── data │ │ ├── db │ │ │ └── DatabaseHelper.kt │ │ ├── model │ │ │ ├── DataOrigin.kt │ │ │ ├── LyricItem.kt │ │ │ ├── LyricResult.kt │ │ │ └── MediaInfo.kt │ │ └── repository │ │ │ ├── ActiveRepository.kt │ │ │ ├── AliasRepository.kt │ │ │ ├── LyricRepository.kt │ │ │ ├── OriginRepository.kt │ │ │ └── ResRepository.kt │ │ ├── hook │ │ ├── BaseHook.kt │ │ ├── MainHook.kt │ │ ├── app │ │ │ └── SystemUI.kt │ │ ├── broadcast │ │ │ └── LyricRequestBroadcastReceiver.kt │ │ ├── helper │ │ │ └── MediaSessionManagerHelper.kt │ │ └── tool │ │ │ ├── EventTool.kt │ │ │ └── Tool.kt │ │ ├── misc │ │ └── Constants.java │ │ ├── modifier │ │ ├── AliasModifier.kt │ │ ├── HiraganaModifier.kt │ │ ├── KatakanaModifier.kt │ │ ├── Modifier.kt │ │ ├── OriginalModifier.kt │ │ ├── RemoveParenthesesModifier.kt │ │ └── SimplifiedModifier.kt │ │ ├── preferences │ │ ├── PackageListAdapter.java │ │ └── PackageListPreference.java │ │ ├── provider │ │ ├── ILrcProvider.java │ │ ├── KugouProvider.java │ │ ├── MusixMatchProvider.java │ │ ├── NeteaseProvider.java │ │ └── QQMusicProvider.java │ │ └── utils │ │ ├── CheckLanguageUtil.java │ │ ├── HttpRequestUtil.kt │ │ ├── LyricSearchUtil.java │ │ └── UnicodeUtil.java │ ├── res │ ├── color-night-v31 │ │ └── settingslib_switch_track_on.xml │ ├── color-v31 │ │ ├── settingslib_surface_light.xml │ │ ├── settingslib_switch_thumb_color.xml │ │ ├── settingslib_switch_track_color.xml │ │ └── settingslib_switch_track_off.xml │ ├── drawable-v24 │ │ ├── ic_add.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_statusbar_icon.xml │ ├── drawable-v31 │ │ ├── settingslib_progress_horizontal.xml │ │ ├── settingslib_switch_bar_bg_disabled.xml │ │ ├── settingslib_switch_bar_bg_off.xml │ │ ├── settingslib_switch_bar_bg_on.xml │ │ ├── settingslib_switch_thumb.xml │ │ └── settingslib_switch_track.xml │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_music.xml │ ├── layout-v31 │ │ ├── collapsing_toolbar_base_layout.xml │ │ └── settingslib_main_switch_bar.xml │ ├── layout │ │ ├── applist_preference_icon.xml │ │ ├── collapsing_toolbar_base_layout.xml │ │ ├── item_lyric.xml │ │ ├── lrcview.xml │ │ ├── settingslib_dropdown_preference.xml │ │ ├── settingslib_icon_frame.xml │ │ ├── settingslib_main_switch_bar.xml │ │ ├── settingslib_main_switch_layout.xml │ │ └── settingslib_preference.xml │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── values-night-v31 │ │ ├── colors.xml │ │ └── themes.xml │ ├── values-night │ │ └── colors.xml │ ├── values-sw600dp │ │ └── dmiens.xml │ ├── values-sw720dp-land │ │ └── dmiens.xml │ ├── values-sw720dp │ │ └── dmiens.xml │ ├── values-v31 │ │ ├── colors.xml │ │ ├── config.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── style_preference.xml │ │ ├── styles.xml │ │ └── themes.xml │ ├── values-zh-rCN │ │ └── strings.xml │ ├── values-zh-rTW │ │ └── strings.xml │ ├── values │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml │ └── xml │ │ ├── network_security_config.xml │ │ └── root_preferences.xml │ └── sqldelight │ └── statusbar │ └── finder │ ├── active.sq │ ├── alias.sq │ ├── origin.sq │ └── res.sq ├── build.gradle ├── docs ├── README_zh-CN.md └── README_zh-TW.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icon.svg ├── img ├── how2use.jpg ├── notificationAccess.jpg └── statusTrue.jpg └── settings.gradle /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 21 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | 26 | # - name: Run Lint 27 | # run: ./gradlew updateLintBaseline 28 | # 29 | # - name: Upload lint-baseline.xml 30 | # uses: actions/upload-artifact@v3 31 | # with: 32 | # name: lint-baseline.xml 33 | # path: ./app/lint-baseline.xml 34 | 35 | - name: Build with Gradle 36 | run: | 37 | ./gradlew assembleDebug 38 | 39 | - name: Upload Debug APK 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: app 43 | path: ./app/build/outputs/apk/* 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | lint-baseline.xml 12 | /.ssh 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LyricsGetterExt 2 | 3 |
4 | icon 5 |

Lyrics Getter · Ext

6 | 7 | ( English / [简体中文](https://github.com/VictorModi/LyricsGetterExt/blob/main/docs/README_zh-CN.md) / [繁體中文](https://github.com/VictorModi/LyricsGetterExt/blob/main/docs/README_zh-TW.md) ) 8 |
9 | 10 | 11 | 12 | # What's this ? 13 | It's a program to get internet lyrics. 14 | 15 | It gets information about the currently playing media via [MediaController](https://developer.android.google.cn/reference/android/media/session/MediaController) and then gets the lyrics via the Internet, the Finally it pushes the lyrics to [Lyrics Getter](https://github.com/xiaowine/Lyric-Getter). 16 | 17 | It's base on [KaguraRinko/StatusBarLyricExt](https://github.com/KaguraRinko/StatusBarLyricExt). But we removed its system detection and added MusixMatch's lyrics source and adapted it to [Lyrics Getter](https://github.com/xiaowine/Lyric-Getter) ! 18 | (...and...Yes...We also added an icon to it) 19 | 20 | # How to use ? 21 | 1. You need to make sure that [LyricsGetter Ext](https://github.com/VictorModi/LyricsGetterExt) is checked in your Xposed Manager for [Lyrics Getter](https://github.com/xiaowine/Lyric-Getter) while Lyrics Getter is running properly. 22 | 23 |
24 | h2u 25 |
26 | 27 | 2. Turn on [LyricsGetter Ext](https://github.com/VictorModi/LyricsGetterExt), turn it on and then grant [LyricsGetter Ext](https://github.com/VictorModi/LyricsGetterExt) notification permission, next you need to make sure that `Lyrics Getter Connection Status` is true and then turn on the switch `Enabled`. 28 | 29 |
30 | st 31 |
32 | 33 | 3. After clicking `Enable`, you will be taken to the Notification Access privilege management page, next, please grant [LyricsGetter Ext](https://github.com/VictorModi/LyricsGetterExt) Notification Next, please grant [LyricsGetter Ext](https://github.com/VictorModi/LyricsGetterExt) Notification Access privileges, and the service will start automatically after the authorization is completed. 34 | 35 |
36 | na 37 |
38 | 39 | 4. Finally, if you are using more than one music app and one of the apps you are using has been adapted by [Lyrics Getter](https://github.com/xiaowine/Lyric-Getter), the lyrics will be duplicated when you use that music app, to avoid this, please click `Input Ignored Apps Rules`, it will get the rules file from [Lyrics Getter](https://github.com/xiaowine/Lyric-Getter) and add all the apps in the rules file to the ignore list automatically, then you can avoid the problem of repeated lyrics output. 40 | 41 | ## Star History 42 | 43 | 44 | 45 | 46 | 47 | Star History Chart 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'app.cash.sqldelight' version '2.0.2' 5 | } 6 | 7 | 8 | android { 9 | defaultConfig { 10 | compileSdk = 35 11 | 12 | applicationId = "statusbar.finder" 13 | minSdkVersion 26 14 | targetSdkVersion 35 15 | versionCode 9 16 | versionName "1.0.9" 17 | } 18 | 19 | compileOptions { 20 | sourceCompatibility JavaVersion.VERSION_21 21 | targetCompatibility JavaVersion.VERSION_21 22 | } 23 | packagingOptions { 24 | resources { 25 | excludes += [ 26 | 'META-INF/DEPENDENCIES', 27 | 'META-INF/NOTICE', 28 | 'META-INF/LICENSE', 29 | 'META-INF/LICENSE.txt', 30 | 'META-INF/NOTICE.txt', 31 | 'META-INF/LICENSE.md', 32 | 'META-INF/LICENSE-notice.md' 33 | ] 34 | } 35 | } 36 | namespace = 'statusbar.finder' 37 | 38 | 39 | buildFeatures { 40 | buildConfig = true 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation "com.android.support:support-compat:28.0.0" 46 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' 47 | implementation 'androidx.appcompat:appcompat:1.7.0' 48 | implementation 'androidx.preference:preference-ktx:1.2.1' 49 | implementation 'com.google.android.material:material:1.12.0' 50 | implementation 'androidx.constraintlayout:constraintlayout:2.2.1' 51 | 52 | implementation group: 'com.github.houbb', name: 'opencc4j', version: '1.8.1' 53 | implementation group: 'io.github.kju2.languagedetector', name: 'language-detector', version: '1.0.5' 54 | implementation 'com.github.xiaowine:Lyric-Getter-Api:6.0.0' 55 | implementation 'org.apache.commons:commons-text:1.13.0' 56 | implementation 'com.andree-surya:moji4j:1.0.0' 57 | implementation 'androidx.core:core-ktx:1.15.0' 58 | compileOnly 'de.robv.android.xposed:api:82' 59 | implementation "com.github.kyuubiran:EzXHelper:2.1.1" 60 | implementation 'com.github.xiaowine:XKT:1.0.12' 61 | implementation "com.github.xiaowine:dsp:1.1.3" 62 | 63 | implementation "app.cash.sqldelight:android-driver:2.0.2" 64 | implementation 'com.google.code.gson:gson:2.10.1' 65 | } 66 | repositories { 67 | google() 68 | mavenCentral() 69 | } 70 | 71 | sqldelight { 72 | databases { 73 | LyricDatabase { 74 | packageName = android.namespace 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 8 | 10 | 11 | 14 | 15 | 16 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 62 | 63 | 66 | 67 | 70 | 73 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | statusbar.finder.hook.MainHook -------------------------------------------------------------------------------- /app/src/main/java/cn/zhaiyifan/lyric/Constants.java: -------------------------------------------------------------------------------- 1 | package cn.zhaiyifan.lyric; 2 | 3 | public class Constants { 4 | public static final String ID_TAG_TITLE = "ti"; 5 | public static final String ID_TAG_ARTIST = "ar"; 6 | public static final String ID_TAG_ALBUM = "al"; 7 | public static final String ID_TAG_CREATOR_LRCFILE = "by"; 8 | public static final String ID_TAG_CREATOR_SONGTEXT = "au"; 9 | public static final String ID_TAG_LENGTH = "length"; 10 | public static final String ID_TAG_OFFSET = "offset"; 11 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/zhaiyifan/lyric/model/Lyric.java: -------------------------------------------------------------------------------- 1 | package cn.zhaiyifan.lyric.model; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import statusbar.finder.data.model.LyricResult; 5 | import statusbar.finder.data.model.MediaInfo; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Comparator; 9 | import java.util.List; 10 | import java.util.Objects; 11 | 12 | public class Lyric { 13 | private static final String TAG = Lyric.class.getSimpleName(); 14 | 15 | public String title; // 歌曲名称 16 | public String artist; // 歌曲作家 17 | public String album; // 歌曲专辑 18 | public String by; 19 | public String author; 20 | public long offset; // 偏移值 21 | public long length; // 歌曲长度 22 | public List sentenceList = new ArrayList<>(100); 23 | public List translatedSentenceList = new ArrayList<>(100); 24 | public MediaInfo originalMediaInfo; 25 | public String packageName; 26 | public LyricResult lyricResult; 27 | 28 | @NotNull 29 | public String toString() { 30 | StringBuilder stringBuilder = new StringBuilder(); 31 | stringBuilder.append("Title: ").append(title).append("\n") 32 | .append("Artist: ").append(artist).append("\n") 33 | .append("Album: ").append(album).append("\n") 34 | .append("By: ").append(by).append("\n") 35 | .append("Author: ").append(author).append("\n") 36 | .append("Length: ").append(length).append("\n") 37 | .append("Offset: ").append(offset).append("\n"); 38 | if (sentenceList != null) { 39 | for (Sentence sentence : sentenceList) { 40 | stringBuilder.append(sentence.toString()).append("\n"); 41 | } 42 | } 43 | if (!translatedSentenceList.isEmpty()) { 44 | stringBuilder.append ("--- Translate Lyrics ---\n"); 45 | for (Sentence sentence : translatedSentenceList) { 46 | stringBuilder.append(sentence.toString()).append("\n"); 47 | } 48 | } 49 | return stringBuilder.toString(); 50 | } 51 | 52 | public void addSentence(List sentenceList,String content, long time) { 53 | sentenceList.add(new Sentence(content, time)); 54 | } 55 | 56 | public static class SentenceComparator implements Comparator { 57 | @Override 58 | public int compare(Sentence sent1, Sentence sent2) { 59 | return (int) (sent1.fromTime - sent2.fromTime); 60 | } 61 | } 62 | 63 | public static class Sentence { 64 | public String content; 65 | public long fromTime; 66 | 67 | public Sentence(String content, long fromTime) { 68 | this.content = content; 69 | this.fromTime = fromTime; 70 | } 71 | 72 | @Override 73 | public boolean equals(Object o) { 74 | if (o == null || getClass() != o.getClass()) return false; 75 | Sentence sentence = (Sentence) o; 76 | return fromTime == sentence.fromTime && Objects.equals(content, sentence.content); 77 | } 78 | 79 | @Override 80 | public int hashCode() { 81 | return Objects.hash(content, fromTime); 82 | } 83 | 84 | @NotNull 85 | public String toString() { 86 | return fromTime + ": " + content; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/settingslib/utils/BuildCompatUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.settingslib.utils; 18 | 19 | import android.os.Build.VERSION; 20 | 21 | /** 22 | * An util class to check whether the current OS version is higher or equal to sdk version of 23 | * device. 24 | */ 25 | public final class BuildCompatUtils { 26 | 27 | /** 28 | * Implementation of BuildCompat.isAtLeast*() suitable for use in Settings 29 | * 30 | *

This still should try using BuildCompat.isAtLeastR() as source of truth, but also checking 31 | * for VERSION_SDK_INT and VERSION.CODENAME in case when BuildCompat implementation returned 32 | * false. Note that both checks should be >= and not = to make sure that when Android version 33 | * increases (i.e., from R to S), this does not stop working. 34 | * 35 | *

Supported configurations: 36 | * 37 | *

    38 | *
  • For current Android release: when new API is not finalized yet (CODENAME = "S", SDK_INT 39 | * = 30|31) 40 | *
  • For current Android release: when new API is finalized (CODENAME = "REL", SDK_INT = 31) 41 | *
  • For next Android release (CODENAME = "T", SDK_INT = 30+) 42 | *
43 | * 44 | *

Note that Build.VERSION_CODES.S cannot be used here until final SDK is available, because 45 | * it is equal to Build.VERSION_CODES.CUR_DEVELOPMENT before API finalization. 46 | * 47 | * @return Whether the current OS version is higher or equal to S. 48 | */ 49 | public static boolean isAtLeastS() { 50 | if (VERSION.SDK_INT < 30) { 51 | return false; 52 | } 53 | 54 | return (VERSION.CODENAME.equals("REL") && VERSION.SDK_INT >= 31) 55 | || (VERSION.CODENAME.length() == 1 56 | && VERSION.CODENAME.compareTo("S") >= 0 57 | && VERSION.CODENAME.compareTo("Z") <= 0); 58 | } 59 | 60 | private BuildCompatUtils() {} 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/settingslib/widget/MainSwitchPreference.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.settingslib.widget; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.util.AttributeSet; 22 | import android.widget.Switch; 23 | import androidx.preference.PreferenceViewHolder; 24 | import androidx.preference.TwoStatePreference; 25 | import statusbar.finder.R; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | /** 31 | * MainSwitchPreference is a Preference with a customized Switch. 32 | * This component is used as the main switch of the page 33 | * to enable or disable the prefereces on the page. 34 | */ 35 | public class MainSwitchPreference extends TwoStatePreference implements OnMainSwitchChangeListener { 36 | 37 | private final List mSwitchChangeListeners = new ArrayList<>(); 38 | 39 | private MainSwitchBar mMainSwitchBar; 40 | private CharSequence mTitle; 41 | 42 | public MainSwitchPreference(Context context) { 43 | super(context); 44 | init(context, null); 45 | } 46 | 47 | public MainSwitchPreference(Context context, AttributeSet attrs) { 48 | super(context, attrs); 49 | init(context, attrs); 50 | } 51 | 52 | public MainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { 53 | super(context, attrs, defStyleAttr); 54 | init(context, attrs); 55 | } 56 | 57 | public MainSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, 58 | int defStyleRes) { 59 | super(context, attrs, defStyleAttr, defStyleRes); 60 | init(context, attrs); 61 | } 62 | 63 | @Override 64 | public void onBindViewHolder(PreferenceViewHolder holder) { 65 | super.onBindViewHolder(holder); 66 | 67 | holder.setDividerAllowedAbove(false); 68 | holder.setDividerAllowedBelow(false); 69 | 70 | mMainSwitchBar = (MainSwitchBar) holder.findViewById(R.id.settingslib_main_switch_bar); 71 | updateStatus(isChecked()); 72 | registerListenerToSwitchBar(); 73 | } 74 | 75 | private void init(Context context, AttributeSet attrs) { 76 | setLayoutResource(R.layout.settingslib_main_switch_layout); 77 | mSwitchChangeListeners.add(this); 78 | if (attrs != null) { 79 | final TypedArray a = context.obtainStyledAttributes(attrs, 80 | androidx.preference.R.styleable.Preference, 0 /*defStyleAttr*/, 81 | 0 /*defStyleRes*/); 82 | final CharSequence title = a.getText( 83 | androidx.preference.R.styleable.Preference_android_title); 84 | setTitle(title); 85 | a.recycle(); 86 | } 87 | } 88 | 89 | @Override 90 | public void setChecked(boolean checked) { 91 | super.setChecked(checked); 92 | if (mMainSwitchBar != null && mMainSwitchBar.isChecked() != checked) { 93 | mMainSwitchBar.setChecked(checked); 94 | } 95 | } 96 | 97 | @Override 98 | public void setTitle(CharSequence title) { 99 | mTitle = title; 100 | if (mMainSwitchBar != null) { 101 | mMainSwitchBar.setTitle(mTitle); 102 | } 103 | } 104 | 105 | @Override 106 | public void onSwitchChanged(Switch switchView, boolean isChecked) { 107 | super.setChecked(isChecked); 108 | } 109 | 110 | /** 111 | * Update the switch status of preference 112 | */ 113 | public void updateStatus(boolean checked) { 114 | setChecked(checked); 115 | if (mMainSwitchBar != null) { 116 | mMainSwitchBar.setTitle(mTitle); 117 | mMainSwitchBar.show(); 118 | } 119 | } 120 | 121 | /** 122 | * Adds a listener for switch changes 123 | */ 124 | public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) { 125 | if (mMainSwitchBar == null) { 126 | mSwitchChangeListeners.add(listener); 127 | } else { 128 | mMainSwitchBar.addOnSwitchChangeListener(listener); 129 | } 130 | } 131 | 132 | /** 133 | * Remove a listener for switch changes 134 | */ 135 | public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) { 136 | if (mMainSwitchBar == null) { 137 | mSwitchChangeListeners.remove(listener); 138 | } else { 139 | mMainSwitchBar.removeOnSwitchChangeListener(listener); 140 | } 141 | } 142 | 143 | private void registerListenerToSwitchBar() { 144 | for (OnMainSwitchChangeListener listener : mSwitchChangeListeners) { 145 | mMainSwitchBar.addOnSwitchChangeListener(listener); 146 | } 147 | mSwitchChangeListeners.clear(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/settingslib/widget/OnMainSwitchChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.settingslib.widget; 18 | 19 | import android.widget.Switch; 20 | 21 | /** 22 | * Called when the checked state of the Switch has changed. 23 | */ 24 | public interface OnMainSwitchChangeListener { 25 | /** 26 | * @param switchView The Switch view whose state has changed. 27 | * @param isChecked The new checked state of switchView. 28 | */ 29 | void onSwitchChanged(Switch switchView, boolean isChecked); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/CSLyricHelper.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder; 2 | 3 | // https://github.com/VictorModi/Lyrics-Getter-Ext/issues/19 4 | // https://github.com/tomakino/CSLyric/blob/main/api.md 5 | 6 | import android.annotation.SuppressLint; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.os.UserHandle; 10 | import android.util.JsonWriter; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.io.IOException; 14 | import java.io.StringWriter; 15 | 16 | /** 17 | * statusbar.finder.CSLyricHelper 提供了一些静态方法来更新和控制歌词显示。 18 | */ 19 | @SuppressLint("MissingPermission") 20 | public class CSLyricHelper { 21 | 22 | 23 | private static Intent createUpdateIntent(PlayInfo playInfo, LyricData lyricData) { 24 | Intent intent = new Intent("com.makino.cslyric.getter.action.LYRIC"); 25 | intent.putExtra("type", "update"); 26 | intent.putExtra("lyricData", lyricData.toString()); 27 | intent.putExtra("playInfo", playInfo.toString()); 28 | return intent; 29 | } 30 | /** 31 | * 发送更新歌词信息的广播。 32 | * 33 | * @param context 上下文,用于发送广播。 34 | * @param playInfo 歌曲播放信息。 35 | * @param lyricData 歌词数据。 36 | */ 37 | public static void updateLyric(Context context, PlayInfo playInfo, LyricData lyricData) { 38 | context.sendBroadcast(createUpdateIntent(playInfo, lyricData)); 39 | } 40 | 41 | public static void updateLyricAsUser(Context context, PlayInfo playInfo, LyricData lyricData, UserHandle user) { 42 | context.sendBroadcastAsUser(createUpdateIntent(playInfo, lyricData), user); 43 | } 44 | 45 | private static Intent createPauseIntent(PlayInfo playInfo) { 46 | Intent intent = new Intent("com.makino.cslyric.getter.action.LYRIC"); 47 | intent.putExtra("type", "pause"); 48 | intent.putExtra("playInfo", playInfo.toString()); 49 | return intent; 50 | } 51 | 52 | /** 53 | * 发送暂停控制信息的广播。 54 | * 55 | * @param context 上下文,用于发送广播。 56 | * @param playInfo 歌曲播放信息。 57 | */ 58 | public static void pause(Context context, PlayInfo playInfo) { 59 | context.sendBroadcast(createPauseIntent(playInfo)); 60 | } 61 | 62 | public static void pauseAsUser(Context context, PlayInfo playInfo, UserHandle user) { 63 | context.sendBroadcastAsUser(createPauseIntent(playInfo), user); 64 | } 65 | 66 | /** 67 | * 发送正在播放信息的广播。 68 | * 69 | * @param context 上下文,用于发送广播。 70 | * @param playInfo 歌曲播放信息。 71 | */ 72 | public static void playing(Context context, PlayInfo playInfo) { 73 | Intent intent = new Intent("com.makino.cslyric.getter.action.LYRIC"); 74 | intent.putExtra("type", "playing"); 75 | intent.putExtra("playInfo", playInfo.toString()); 76 | context.sendBroadcast(intent); 77 | } 78 | 79 | /** 80 | * LyricData 类用于封装歌词信息,并实现其字符串表示形式的生成。 81 | */ 82 | public static class LyricData { 83 | 84 | public String lyric; 85 | 86 | public LyricData(String lyric) { 87 | this.lyric = lyric; 88 | } 89 | 90 | @NotNull 91 | @Override 92 | public String toString() { 93 | StringWriter sw = new StringWriter(); 94 | try (JsonWriter jw = new JsonWriter(sw)) { 95 | jw.beginObject(); 96 | jw.name("lyric").value(lyric); 97 | jw.endObject(); 98 | } catch (IOException e) { 99 | e.fillInStackTrace(); 100 | } 101 | return sw.toString(); 102 | } 103 | } 104 | 105 | /** 106 | * PlayInfo 类用于封装播放信息,并实现其字符串表示形式的生成。 107 | */ 108 | public static class PlayInfo { 109 | 110 | public String icon; 111 | 112 | public String packageName; 113 | 114 | public boolean isPlaying; 115 | 116 | public PlayInfo(String icon, String packageName) { 117 | this.icon = icon; 118 | this.packageName = packageName; 119 | } 120 | 121 | @NotNull 122 | @Override 123 | public String toString() { 124 | StringWriter sw = new StringWriter(); 125 | try (JsonWriter jw = new JsonWriter(sw)) { 126 | jw.beginObject(); 127 | jw.name("icon").value(icon); 128 | jw.name("packageName").value(packageName); 129 | jw.name("isPlaying").value(isPlaying); 130 | jw.endObject(); 131 | } catch (IOException e) { 132 | e.fillInStackTrace(); 133 | } 134 | return sw.toString(); 135 | } 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/LrcGetter.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder 2 | 3 | import android.content.Context 4 | import android.media.MediaMetadata 5 | import cn.zhaiyifan.lyric.LyricUtils 6 | import cn.zhaiyifan.lyric.model.Lyric 7 | import com.github.houbb.opencc4j.util.ZhConverterUtil 8 | import statusbar.finder.app.event.LyricsResultChange 9 | import statusbar.finder.app.event.LyricsResultChange.Companion.getInstance 10 | import statusbar.finder.data.db.DatabaseHelper.init 11 | import statusbar.finder.data.model.DataOrigin 12 | import statusbar.finder.data.model.LyricResult 13 | import statusbar.finder.data.model.MediaInfo 14 | import statusbar.finder.data.repository.ActiveRepository.insertActiveLog 15 | import statusbar.finder.data.repository.LyricRepository.getActiveLyricFromDatabase 16 | import statusbar.finder.data.repository.LyricRepository.getActiveLyricFromDatabaseByOriginId 17 | import statusbar.finder.data.repository.ResRepository.getResByOriginIdAndProvider 18 | import statusbar.finder.data.repository.ResRepository.insertResData 19 | import statusbar.finder.modifier.* 20 | import statusbar.finder.provider.KugouProvider 21 | import statusbar.finder.provider.MusixMatchProvider 22 | import statusbar.finder.provider.NeteaseProvider 23 | import statusbar.finder.provider.QQMusicProvider 24 | import statusbar.finder.utils.CheckLanguageUtil 25 | import statusbar.finder.utils.LyricSearchUtil 26 | import java.io.IOException 27 | 28 | /** 29 | * LyricGetterExt - statusbar.finder 30 | * @description TODO: coming soon. 31 | * @author VictorModi 32 | * @email victormodi@outlook.com 33 | * @date 2025/3/13 17:23 34 | */ 35 | object LrcGetter { 36 | private val providers = arrayOf( 37 | NeteaseProvider(), 38 | KugouProvider(), 39 | QQMusicProvider(), 40 | MusixMatchProvider() 41 | ) 42 | 43 | private val modifiers = arrayOf( 44 | AliasModifier(), 45 | SimplifiedModifier(), 46 | RemoveParenthesesModifier(), 47 | OriginalModifier(), 48 | KatakanaModifier(), 49 | HiraganaModifier(), 50 | ) 51 | 52 | fun getLyric(context: Context, mediaMetadata: MediaMetadata, sysLang: String, packageName: String): Lyric? { 53 | return getLyric(context, MediaInfo(mediaMetadata), sysLang, packageName) 54 | } 55 | 56 | private fun getLyric(context: Context, mediaInfo: MediaInfo, sysLang: String, packageName: String): Lyric? { 57 | init(context) 58 | val databaseResult = getActiveLyricFromDatabase(mediaInfo, packageName) 59 | var currentResult: LyricResult? = databaseResult.first 60 | currentResult?.let { 61 | getInstance().notifyResult(LyricsResultChange.Data(mediaInfo, it)) 62 | return LyricUtils.parseLyric(it, mediaInfo, packageName) 63 | } 64 | for (modifier in modifiers) { 65 | modifier.modify(mediaInfo, databaseResult.second)?.let { 66 | searchLyricsResultByInfo( 67 | it, 68 | databaseResult.second, 69 | sysLang, 70 | ) 71 | } 72 | currentResult = getActiveLyricFromDatabaseByOriginId(databaseResult.second) 73 | currentResult?.let { 74 | it.matchBy = modifier.javaClass.simpleName.removeSuffix("Modifier") 75 | it.dataOrigin = DataOrigin.INTERNET 76 | getInstance().notifyResult(LyricsResultChange.Data(mediaInfo, it)) 77 | return LyricUtils.parseLyric(it, mediaInfo, packageName) 78 | } 79 | } 80 | return null 81 | } 82 | 83 | private fun searchLyricsResultByInfo(mediaInfo: MediaInfo, originId: Long, sysLang: String) { 84 | var bestMatchSource: String? = null 85 | var bestMatchDistance: Long = Long.MAX_VALUE 86 | for (provider in providers) { 87 | try { 88 | val lyricResult = provider.getLyric(mediaInfo) 89 | lyricResult?.source = provider.javaClass.simpleName.removeSuffix("Provider") 90 | if (lyricResult?.lyric != null) { 91 | val allLyrics = if (lyricResult.translatedLyric != null) { 92 | LyricUtils.getAllLyrics(false, lyricResult.translatedLyric) 93 | } else { 94 | LyricUtils.getAllLyrics(false, lyricResult.lyric) 95 | } 96 | if (!CheckLanguageUtil.isJapanese(allLyrics)) { 97 | when (sysLang) { 98 | "zh-CN" -> if (lyricResult.translatedLyric != null) { 99 | lyricResult.translatedLyric = ZhConverterUtil.toSimple(lyricResult.translatedLyric) 100 | } else { 101 | lyricResult.lyric = ZhConverterUtil.toSimple(lyricResult.lyric) 102 | } 103 | 104 | "zh-TW" -> if (lyricResult.translatedLyric != null) { 105 | lyricResult.translatedLyric = 106 | ZhConverterUtil.toTraditional(lyricResult.translatedLyric) 107 | } else { 108 | lyricResult.lyric = ZhConverterUtil.toTraditional(lyricResult.lyric) 109 | } 110 | 111 | else -> {} 112 | } 113 | } 114 | insertResData(originId, lyricResult) 115 | if (LyricSearchUtil.isLyricContent(lyricResult.lyric) 116 | && lyricResult.distance < bestMatchDistance 117 | ) { 118 | bestMatchSource = lyricResult.source 119 | bestMatchDistance = lyricResult.distance 120 | } 121 | } 122 | } catch (e: IOException) { 123 | e.fillInStackTrace() 124 | } 125 | } 126 | if (bestMatchSource != null) { 127 | val bestMatchResult = checkNotNull(getResByOriginIdAndProvider(originId, bestMatchSource)) 128 | insertActiveLog(originId, bestMatchResult.id) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/App.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app 2 | 3 | import android.app.Application 4 | import android.util.Log 5 | import cn.xiaowine.dsp.DSP 6 | import cn.xiaowine.dsp.data.MODE 7 | import statusbar.finder.BuildConfig 8 | import statusbar.finder.hook.tool.Tool.xpActivation 9 | 10 | /** 11 | * LyricGetterExt - statusbar.finder.app 12 | * @description TODO: coming soon. 13 | * @author VictorModi 14 | * @email victormodi@outlook.com 15 | * @date 2025/2/18 23:10 16 | */ 17 | class App : Application() { 18 | override fun onCreate() { 19 | super.onCreate() 20 | xpActivation = DSP.init(this, BuildConfig.APPLICATION_ID, MODE.HOOK, false) 21 | Log.i("DSP", "xpActivation $xpActivation") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/LyricsAdapter.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import android.graphics.Color 6 | import android.graphics.Typeface 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.TextView 11 | import androidx.recyclerview.widget.RecyclerView 12 | import statusbar.finder.R 13 | import statusbar.finder.data.model.LyricItem 14 | 15 | /** 16 | * LyricGetterExt - statusbar.finder 17 | * @description TODO: coming soon. 18 | * @author VictorModi 19 | * @email victormodi@outlook.com 20 | * @date 2025/2/8 11:01 21 | */ 22 | class LyricsAdapter(private val lyrics: List) : 23 | RecyclerView.Adapter() { 24 | 25 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 26 | var sb = R.id.tvOrigin 27 | val tvOrigin: TextView = view.findViewById(R.id.tvOrigin) 28 | val tvTranslation: TextView = view.findViewById(R.id.tvTranslation) 29 | } 30 | 31 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 32 | val item = lyrics[position] 33 | 34 | // 设置文本内容 35 | holder.tvOrigin.text = item.origin 36 | item.translation?.let { 37 | holder.tvTranslation.text = it 38 | holder.tvTranslation.visibility = View.VISIBLE 39 | } ?: run { 40 | holder.tvTranslation.visibility = View.GONE 41 | } 42 | 43 | // 高亮样式 44 | val context = holder.itemView.context 45 | val isDark = isDarkMode(context) 46 | val highlightColor = if (isDark) Color.WHITE else Color.BLACK 47 | val normalColor = if (isDark) Color.GRAY else Color.LTGRAY 48 | 49 | if (item.isHighlight) { 50 | holder.tvOrigin.setTextColor(highlightColor) 51 | holder.tvOrigin.textSize = 20f 52 | holder.tvOrigin.typeface = Typeface.DEFAULT_BOLD 53 | 54 | holder.tvTranslation.setTextColor(highlightColor) 55 | holder.tvTranslation.textSize = 16f 56 | } else { 57 | holder.tvOrigin.setTextColor(normalColor) 58 | holder.tvOrigin.textSize = 16f 59 | holder.tvOrigin.typeface = Typeface.DEFAULT 60 | 61 | holder.tvTranslation.setTextColor(normalColor) 62 | holder.tvTranslation.textSize = 14f 63 | } 64 | } 65 | 66 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 67 | val view = LayoutInflater.from(parent.context) 68 | .inflate(R.layout.item_lyric, parent, false) 69 | return ViewHolder(view) 70 | } 71 | 72 | override fun getItemCount() = lyrics.size 73 | 74 | private fun isDarkMode(context: Context): Boolean { 75 | return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/broadcast/LyricsBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app.broadcast 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import com.google.gson.Gson 7 | import statusbar.finder.app.event.LyricSentenceUpdate 8 | import statusbar.finder.app.event.LyricsChange 9 | import statusbar.finder.misc.Constants.BROADCAST_LYRICS_CHANGED 10 | import statusbar.finder.misc.Constants.BROADCAST_LYRIC_SENTENCE_UPDATE 11 | 12 | /** 13 | * LyricGetterExt - statusbar.finder.app 14 | * @description TODO: coming soon. 15 | * @author VictorModi 16 | * @email victormodi@outlook.com 17 | * @date 2025/3/1 11:53 18 | */ 19 | class LyricsBroadcastReceiver : BroadcastReceiver() { 20 | 21 | private val gson by lazy { Gson() } 22 | 23 | override fun onReceive(context: Context?, intent: Intent?) { 24 | intent?.getStringExtra("data")?.let { json -> 25 | when (intent.action) { 26 | BROADCAST_LYRICS_CHANGED -> { 27 | val data = gson.fromJson(json, LyricsChange.Data::class.java) 28 | LyricsChange.getInstance().notifyResult(data) 29 | } 30 | BROADCAST_LYRIC_SENTENCE_UPDATE -> { 31 | val data = gson.fromJson(json, LyricSentenceUpdate.Data::class.java) 32 | LyricSentenceUpdate.getInstance().notifyLyrics(data) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/event/AppsListChanged.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app.event 2 | 3 | import androidx.lifecycle.LiveData 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder.livedata 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/2/8 19:55 11 | */ 12 | class AppsListChanged private constructor() : LiveData() { 13 | 14 | companion object { 15 | @Volatile 16 | private var instance: AppsListChanged? = null 17 | 18 | fun getInstance(): AppsListChanged { 19 | return instance ?: synchronized(this) { 20 | instance ?: AppsListChanged().also { instance = it } 21 | } 22 | } 23 | } 24 | 25 | fun notifyChange() { 26 | postValue(null) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/event/LyricSentenceUpdate.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app.event 2 | 3 | import androidx.lifecycle.LiveData 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder.livedata 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/2/8 19:56 11 | */ 12 | 13 | class LyricSentenceUpdate private constructor() : LiveData() { 14 | 15 | companion object { 16 | @Volatile 17 | private var instance: LyricSentenceUpdate? = null 18 | 19 | fun getInstance(): LyricSentenceUpdate { 20 | return instance ?: synchronized(this) { 21 | instance ?: LyricSentenceUpdate().also { instance = it } 22 | } 23 | } 24 | } 25 | 26 | fun notifyLyrics(data: Data) { 27 | postValue(data) 28 | } 29 | 30 | data class Data( 31 | val lyric: String, 32 | val translatedLyric: String?, 33 | val lyricsIndex: Int, 34 | val delay: Int 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/event/LyricsChange.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app.event 2 | 3 | import androidx.lifecycle.LiveData 4 | import cn.zhaiyifan.lyric.model.Lyric 5 | 6 | /** 7 | * LyricGetterExt - statusbar.finder.livedata 8 | * @description TODO: coming soon. 9 | * @author VictorModi 10 | * @email victormodi@outlook.com 11 | * @date 2025/2/8 20:13 12 | */ 13 | class LyricsChange private constructor() : LiveData() { 14 | 15 | companion object { 16 | @Volatile 17 | private var instance: LyricsChange? = null 18 | 19 | fun getInstance(): LyricsChange { 20 | return instance ?: synchronized(this) { 21 | instance ?: LyricsChange().also { instance = it } 22 | } 23 | } 24 | } 25 | 26 | fun notifyResult(data: Data) { 27 | postValue(data) 28 | } 29 | 30 | data class Data( 31 | val lyric: Lyric?, 32 | val providers: Map?, 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/app/event/LyricsResultChange.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.app.event 2 | 3 | /** 4 | * LyricGetterExt - statusbar.finder.livedata 5 | * @description TODO: coming soon. 6 | * @author VictorModi 7 | * @email victormodi@outlook.com 8 | * @date 2025/2/8 19:57 9 | */ 10 | import androidx.lifecycle.LiveData 11 | import statusbar.finder.data.model.LyricResult 12 | import statusbar.finder.data.model.MediaInfo 13 | 14 | class LyricsResultChange private constructor() : LiveData() { 15 | 16 | companion object { 17 | @Volatile 18 | private var instance: LyricsResultChange? = null 19 | 20 | fun getInstance(): LyricsResultChange { 21 | return instance ?: synchronized(this) { 22 | instance ?: LyricsResultChange().also { instance = it } 23 | } 24 | } 25 | } 26 | 27 | fun notifyResult(data: Data) { 28 | postValue(data) 29 | } 30 | 31 | data class Data( 32 | val originInfo: MediaInfo, 33 | val result: LyricResult? 34 | ) 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/config/Config.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.config 2 | 3 | import cn.xiaowine.dsp.delegate.Delegate.serial 4 | 5 | class Config { 6 | var translateDisplayType: String by serial("origin") 7 | var targetPackages: String by serial("") 8 | var forceRepeat: Boolean by serial(false) 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/db/DatabaseHelper.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.db 2 | 3 | import android.content.Context 4 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver 5 | import statusbar.finder.LyricDatabase 6 | 7 | /** 8 | * LyricGetterExt - statusbar 9 | * @description TODO: coming soon. 10 | * @author VictorModi 11 | * @email victormodi@outlook.com 12 | * @date 2025/2/9 13:42 13 | */ 14 | object DatabaseHelper { 15 | private const val DATABASE_NAME = "lyric.db" 16 | private lateinit var database: LyricDatabase 17 | 18 | @Synchronized 19 | fun init(context: Context) { 20 | if (DatabaseHelper::database.isInitialized) return 21 | val driver = AndroidSqliteDriver(LyricDatabase.Schema, context, DATABASE_NAME) 22 | database = LyricDatabase(driver) 23 | } 24 | 25 | fun getDatabase(): LyricDatabase { 26 | if (!DatabaseHelper::database.isInitialized) { 27 | throw IllegalStateException("DatabaseHelper.init() must be called before using the database.") 28 | } 29 | return database 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/model/DataOrigin.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.model 2 | 3 | /** 4 | * LyricGetterExt - statusbar.finder.data 5 | * @description TODO: coming soon. 6 | * @author VictorModi 7 | * @email victormodi@outlook.com 8 | * @date 2025/2/9 14:02 9 | */ 10 | enum class DataOrigin { 11 | UNDEFINED, 12 | INTERNET, 13 | DATABASE; 14 | 15 | fun getCapitalizedName(): String { 16 | return name.lowercase().replaceFirstChar { it.uppercase() } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/model/LyricItem.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.model 2 | 3 | /** 4 | * LyricGetterExt - statusbar.finder 5 | * @description TODO: coming soon. 6 | * @author VictorModi 7 | * @email victormodi@outlook.com 8 | * @date 2025/2/8 11:00 9 | */ 10 | data class LyricItem( 11 | val origin: String, 12 | val translation: String? = null, 13 | var isHighlight: Boolean = false, 14 | ) 15 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/model/LyricResult.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.model 2 | 3 | import statusbar.finder.Res 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder.data 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/2/9 14:00 11 | */ 12 | data class LyricResult( 13 | var lyric: String? = null, 14 | var translatedLyric: String? = null, 15 | var distance: Long = 0, 16 | var source: String = "Local", 17 | var offset: Int = 0, 18 | var resultInfo: MediaInfo? = null, 19 | var resId: Long = -1, 20 | var originId: Long = -1, 21 | var dataOrigin: DataOrigin = DataOrigin.UNDEFINED, 22 | var matchBy: String? = null 23 | ) { 24 | override fun toString(): String { 25 | return """ 26 | Distance: $distance 27 | Source: $source 28 | Offset: $offset 29 | Lyric: $lyric 30 | TranslatedLyric: $translatedLyric 31 | ResultInfo: $resultInfo 32 | """.trimIndent() 33 | } 34 | 35 | constructor(result: Res) : this( 36 | lyric = result.lyric, 37 | translatedLyric = result.translated_lyric, 38 | distance = result.distance ?: -1, 39 | offset = result.lyric_offset.toInt(), 40 | source = result.provider, 41 | dataOrigin = DataOrigin.DATABASE, 42 | resId = result.id, 43 | originId = result.origin_id, 44 | resultInfo = MediaInfo( 45 | title = result.title ?: "", 46 | artist = result.artist ?: "", 47 | album = result.album ?: "", 48 | distance = result.distance ?: -1, 49 | duration = -1 50 | ) 51 | ) 52 | } 53 | 54 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/model/MediaInfo.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.model 2 | 3 | import android.media.MediaMetadata 4 | import com.github.houbb.opencc4j.util.ZhConverterUtil 5 | import statusbar.finder.Origin 6 | 7 | /** 8 | * LyricGetterExt - statusbar.finder.data 9 | * @description TODO: coming soon. 10 | * @author VictorModi 11 | * @email victormodi@outlook.com 12 | * @date 2025/2/9 14:03 13 | */ 14 | data class MediaInfo( 15 | var title: String = "", 16 | var artist: String = "", 17 | var album: String = "", 18 | var duration: Long = -1, 19 | var distance: Long = -1 20 | ) { 21 | constructor(mediaMetadata: MediaMetadata) : this( 22 | title = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "", 23 | artist = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "", 24 | album = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "", 25 | duration = mediaMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION), 26 | distance = -1 27 | ) 28 | 29 | constructor(originData: Origin) : this( 30 | title = originData.title, 31 | artist = originData.artist, 32 | album = originData.album ?: "", 33 | duration = originData.duration ?: -1, 34 | distance = -1 35 | ) 36 | 37 | fun toSimple(): MediaInfo { 38 | return this.copy( 39 | title = ZhConverterUtil.toSimple(this.title), 40 | artist = ZhConverterUtil.toSimple(this.artist), 41 | album = ZhConverterUtil.toSimple(this.album) 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/repository/ActiveRepository.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.repository 2 | 3 | import statusbar.finder.ActiveQueries 4 | import statusbar.finder.data.db.DatabaseHelper 5 | 6 | /** 7 | * LyricGetterExt - statusbar.finder.data.repository 8 | * @description TODO: coming soon. 9 | * @author VictorModi 10 | * @email victormodi@outlook.com 11 | * @date 2025/2/16 23:07 12 | */ 13 | object ActiveRepository { 14 | private val queries: ActiveQueries by lazy { 15 | DatabaseHelper.getDatabase().activeQueries 16 | } 17 | 18 | fun insertActiveLog(originId: Long, resultId: Long) { 19 | queries.insertActive(originId, resultId) 20 | } 21 | 22 | fun getResultIdByOriginId(originId: Long): Long? { 23 | return queries.getResultId(originId).executeAsOneOrNull() 24 | } 25 | 26 | fun updateResultIdByOriginId(originId: Long, resultId: Long) { 27 | queries.updateActive(resultId, originId) 28 | } 29 | 30 | fun deleteResultByOriginId(originId: Long) { 31 | queries.deleteActiveByOriginId(originId) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/repository/AliasRepository.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.repository 2 | 3 | import statusbar.finder.Alias 4 | import statusbar.finder.AliasQueries 5 | import statusbar.finder.data.db.DatabaseHelper 6 | 7 | /** 8 | * LyricGetterExt - statusbar.finder.data.repository 9 | * @description TODO: coming soon. 10 | * @author VictorModi 11 | * @email victormodi@outlook.com 12 | * @date 2025/3/29 17:17 13 | */ 14 | object AliasRepository { 15 | private val queries: AliasQueries by lazy { 16 | DatabaseHelper.getDatabase().aliasQueries 17 | } 18 | 19 | fun getAlias(originId: Long): Alias? { 20 | return queries.getAlias(originId).executeAsOneOrNull() 21 | } 22 | 23 | fun updateAlias(originId: Long, title: String?, artist: String?, album: String?) { 24 | getAlias(originId)?.let { 25 | queries.updateAlias( 26 | originId, 27 | title ?: it.title, 28 | artist ?: it.artist, 29 | album ?: it.album 30 | ) 31 | } ?: run { 32 | queries.updateAlias(originId, title, artist, album) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/repository/LyricRepository.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.repository 2 | 3 | import statusbar.finder.data.model.LyricResult 4 | import statusbar.finder.data.model.MediaInfo 5 | 6 | /** 7 | * LyricGetterExt - statusbar.finder.data.repository 8 | * @description TODO: coming soon. 9 | * @author VictorModi 10 | * @email victormodi@outlook.com 11 | * @date 2025/2/16 23:01 12 | */ 13 | object LyricRepository { 14 | /** 15 | * 根据 MediaInfo 获取激活的歌词 & OriginId 16 | */ 17 | fun getActiveLyricFromDatabase(mediaInfo: MediaInfo, packageName: String): Pair { 18 | val originId = OriginRepository.insertOrGetMediaInfoId(mediaInfo, packageName) 19 | val lyricResult = getActiveLyricFromDatabaseByOriginId(originId) 20 | return Pair(lyricResult, originId) 21 | } 22 | 23 | /** 24 | * 根据 OriginId 获取激活的歌词 25 | */ 26 | fun getActiveLyricFromDatabaseByOriginId(originId: Long): LyricResult? { 27 | val resultId = ActiveRepository.getResultIdByOriginId(originId) ?: return null 28 | val res = ResRepository.getResById(resultId) ?: return null 29 | return LyricResult(res) 30 | } 31 | 32 | 33 | fun deleteResByOriginIdAndDeleteActive(originId: Long) { 34 | ActiveRepository.deleteResultByOriginId(originId) 35 | ResRepository.deleteResByOriginId(originId) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/repository/OriginRepository.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.repository 2 | 3 | import android.database.sqlite.SQLiteConstraintException 4 | import statusbar.finder.OriginQueries 5 | import statusbar.finder.data.db.DatabaseHelper 6 | import statusbar.finder.data.model.MediaInfo 7 | 8 | /** 9 | * LyricGetterExt - statusbar.finder.data.repository 10 | * @description TODO: coming soon. 11 | * @author VictorModi 12 | * @email victormodi@outlook.com 13 | * @date 2025/2/16 23:08 14 | */ 15 | object OriginRepository { 16 | private val queries: OriginQueries by lazy { 17 | DatabaseHelper.getDatabase().originQueries 18 | } 19 | 20 | fun insertOrGetMediaInfoId(mediaInfo: MediaInfo, packageName: String): Long { 21 | getOriginId(mediaInfo, packageName)?.let { return it } 22 | return try { 23 | queries.insertMediaInfo( 24 | mediaInfo.title, 25 | mediaInfo.artist, 26 | mediaInfo.album, 27 | mediaInfo.duration, 28 | packageName 29 | ) 30 | queries.getLastInsertId().executeAsOne() 31 | } catch (e: SQLiteConstraintException) { 32 | e.printStackTrace() 33 | -1L 34 | } 35 | } 36 | 37 | private fun getOriginId(mediaInfo: MediaInfo, packageName: String): Long? { 38 | return queries.getMediaInfoId( 39 | mediaInfo.title, 40 | mediaInfo.artist, 41 | mediaInfo.album, 42 | packageName 43 | ).executeAsOneOrNull() 44 | } 45 | 46 | fun getMediaInfoById(id: Long): MediaInfo? { 47 | return queries.getInfoById(id).executeAsOneOrNull()?.let { info -> 48 | MediaInfo().apply { 49 | title = info.title 50 | artist = info.artist 51 | album = info.album ?: "" 52 | duration = info.duration ?: -1L 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/data/repository/ResRepository.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.data.repository 2 | 3 | import android.database.sqlite.SQLiteConstraintException 4 | import statusbar.finder.Res 5 | import statusbar.finder.ResQueries 6 | import statusbar.finder.data.db.DatabaseHelper 7 | import statusbar.finder.data.model.LyricResult 8 | 9 | /** 10 | * LyricGetterExt - statusbar.finder.data.repository 11 | * @description TODO: coming soon. 12 | * @author VictorModi 13 | * @email victormodi@outlook.com 14 | * @date 2025/2/16 23:09 15 | */ 16 | object ResRepository { 17 | private val queries: ResQueries by lazy { 18 | DatabaseHelper.getDatabase().resQueries 19 | } 20 | 21 | fun insertResData(originId: Long, lyricResult: LyricResult) { 22 | val resultInfo = lyricResult.resultInfo 23 | ?: throw IllegalArgumentException("LyricResult.mResultInfo cannot be null") 24 | try { 25 | queries.insertRes( 26 | originId, 27 | lyricResult.source, 28 | lyricResult.lyric, 29 | lyricResult.translatedLyric, 30 | lyricResult.distance, 31 | resultInfo.title, 32 | resultInfo.artist, 33 | resultInfo.album 34 | ) 35 | } catch (e: SQLiteConstraintException) { 36 | e.printStackTrace() 37 | } 38 | } 39 | 40 | fun getResByOriginId(originId: Long): List { 41 | return queries.getResByOriginId(originId).executeAsList() 42 | } 43 | 44 | fun getResById(resId: Long): Res? { 45 | return queries.getResById(resId).executeAsOneOrNull() 46 | } 47 | 48 | fun updateResOffsetById(id: Long, offset: Long) { 49 | return queries.updateResOffset(offset, id) 50 | } 51 | 52 | fun getResByOriginIdAndProvider(originId: Long, provider: String): Res? { 53 | return queries.getResByIdAndProvider(originId, provider).executeAsOneOrNull() 54 | } 55 | 56 | fun getProvidersMapByOriginId(originId: Long): Map { 57 | return queries.getProvidersAndIdByOriginId(originId).executeAsList().associate { it.provider to it.id } 58 | } 59 | 60 | fun deleteResByOriginId(originId: Long) { 61 | return queries.deleteResByOriginId(originId) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/BaseHook.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook 2 | 3 | import cn.xiaowine.dsp.DSP 4 | import cn.xiaowine.dsp.data.MODE 5 | import com.github.kyuubiran.ezxhelper.Log 6 | import statusbar.finder.BuildConfig 7 | 8 | abstract class BaseHook { 9 | var isInit: Boolean = false 10 | private var isDSPEnabled: Boolean = false 11 | open fun init() { 12 | isDSPEnabled = DSP.init(null, BuildConfig.APPLICATION_ID, MODE.HOOK, true) 13 | Log.i("${BuildConfig.APPLICATION_ID} isDSPEnabled: $isDSPEnabled") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/MainHook.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook 2 | 3 | 4 | import com.github.kyuubiran.ezxhelper.EzXHelper 5 | import com.github.kyuubiran.ezxhelper.Log 6 | import de.robv.android.xposed.IXposedHookLoadPackage 7 | import de.robv.android.xposed.IXposedHookZygoteInit 8 | import de.robv.android.xposed.callbacks.XC_LoadPackage 9 | import statusbar.finder.hook.app.SystemUI 10 | 11 | 12 | /** 13 | * LyricGetterExt - statusbar.finder.hook 14 | * @description Coming soon. 15 | * @author VictorModi 16 | * @email victormodi@outlook.com 17 | * @date 2024/2/23 0:17 18 | */ 19 | class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit { 20 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 21 | EzXHelper.initHandleLoadPackage(lpparam) 22 | when (lpparam.packageName) { 23 | "com.android.systemui" -> initHooks(SystemUI) 24 | else -> return 25 | } 26 | } 27 | 28 | 29 | override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) { 30 | EzXHelper.initZygote(startupParam) 31 | } 32 | 33 | private fun initHooks(vararg hook: BaseHook) { 34 | hook.forEach { 35 | try { 36 | if (it.isInit) return@forEach 37 | it.init() 38 | it.isInit = true 39 | Log.i("Init hook ${it.javaClass.name} completed") 40 | } catch (e: Exception) { 41 | e.printStackTrace() 42 | Log.i("Init hook ${it.javaClass.name} failed") 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/app/SystemUI.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook.app 2 | 3 | import android.app.Application 4 | import android.content.IntentFilter 5 | import com.github.kyuubiran.ezxhelper.HookFactory.`-Static`.createHook 6 | import com.github.kyuubiran.ezxhelper.finders.MethodFinder.`-Static`.methodFinder 7 | import statusbar.finder.hook.BaseHook 8 | import statusbar.finder.hook.broadcast.LyricRequestBroadcastReceiver 9 | import statusbar.finder.hook.helper.MediaSessionManagerHelper 10 | import statusbar.finder.misc.Constants.* 11 | 12 | /** 13 | * LyricGetterExt - statusbar.finder.hook.app 14 | * @description TODO: coming soon. 15 | * @author VictorModi 16 | * @email victormodi@outlook.com 17 | * @date 2025/1/21 下午3:17 18 | */ 19 | object SystemUI : BaseHook() { 20 | 21 | override fun init() { 22 | super.init() 23 | Application::class.java.methodFinder().filterByName("attach").first().createHook { 24 | after { 25 | val application = it.thisObject as Application 26 | MediaSessionManagerHelper.init(application) 27 | application.registerReceiver( 28 | LyricRequestBroadcastReceiver(), 29 | IntentFilter().apply { 30 | addAction(BROADCAST_LYRICS_CHANGED_REQUEST) 31 | addAction(BROADCAST_LYRICS_OFFSET_UPDATE_REQUEST) 32 | addAction(BROADCAST_LYRICS_ACTIVE_UPDATE_REQUEST) 33 | addAction(BROADCAST_LYRICS_DELETE_RESULT_REQUEST) 34 | addAction(BROADCAST_LYRICS_UPDATE_ALIAS_REQUEST) 35 | } 36 | ) 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/broadcast/LyricRequestBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook.broadcast 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.os.UserHandle 8 | import statusbar.finder.data.repository.ActiveRepository 9 | import statusbar.finder.data.repository.AliasRepository 10 | import statusbar.finder.data.repository.LyricRepository.deleteResByOriginIdAndDeleteActive 11 | import statusbar.finder.data.repository.ResRepository 12 | import statusbar.finder.hook.helper.MediaSessionManagerHelper.getLastBroadcastIntent 13 | import statusbar.finder.hook.helper.MediaSessionManagerHelper.updateLyrics 14 | import statusbar.finder.misc.Constants.* 15 | 16 | /** 17 | * LyricGetterExt - statusbar.finder.hook.broadcast 18 | * @description TODO: coming soon. 19 | * @author VictorModi 20 | * @email victormodi@outlook.com 21 | * @date 2025/3/12 13:07 22 | */ 23 | @SuppressLint("MissingPermission") 24 | class LyricRequestBroadcastReceiver : BroadcastReceiver() { 25 | private val user: UserHandle = UserHandle.getUserHandleForUid(android.os.Process.myUid()) 26 | 27 | override fun onReceive(context: Context, intent: Intent) { 28 | when (intent.action) { 29 | BROADCAST_LYRICS_CHANGED_REQUEST -> { 30 | getLastBroadcastIntent()?.let { 31 | context.sendBroadcastAsUser(it.left, user) 32 | it.right?.let { lineIntent -> 33 | context.sendBroadcastAsUser(lineIntent, user) 34 | } 35 | } 36 | } 37 | BROADCAST_LYRICS_OFFSET_UPDATE_REQUEST -> { 38 | val offset = intent.getLongExtra("offset", -1L) 39 | val resId = intent.getLongExtra("resId", -1L) 40 | val packageName = intent.getStringExtra("packageName") 41 | packageName?.let { 42 | if (offset != -1L && resId != -1L) { 43 | ResRepository.updateResOffsetById(resId, offset) 44 | updateLyrics(packageName) 45 | } 46 | } 47 | } 48 | BROADCAST_LYRICS_ACTIVE_UPDATE_REQUEST -> { 49 | val originId = intent.getLongExtra("originId", -1L) 50 | val resId = intent.getLongExtra("resId", -1L) 51 | val packageName = intent.getStringExtra("packageName") 52 | packageName?.let { 53 | if (originId != -1L && resId != -1L) { 54 | ActiveRepository.updateResultIdByOriginId(originId, resId) 55 | updateLyrics(packageName) 56 | } 57 | } 58 | } 59 | BROADCAST_LYRICS_DELETE_RESULT_REQUEST -> { 60 | val originId = intent.getLongExtra("originId", -1L) 61 | val packageName = intent.getStringExtra("packageName") 62 | packageName?.let { 63 | if (originId != -1L) { 64 | deleteResByOriginIdAndDeleteActive(originId) 65 | updateLyrics(packageName) 66 | } 67 | } 68 | } 69 | BROADCAST_LYRICS_UPDATE_ALIAS_REQUEST -> { 70 | val originId = intent.getLongExtra("originId", -1L) 71 | val packageName = intent.getStringExtra("packageName") 72 | val newTitle = intent.getStringExtra("newTitle") 73 | val newArtist = intent.getStringExtra("newArtist") 74 | val newAlbum = intent.getStringExtra("newAlbum") 75 | 76 | packageName?.let { 77 | AliasRepository.updateAlias(originId, newTitle, newArtist, newAlbum) 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/tool/EventTool.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook.tool 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.UserHandle 7 | import cn.lyric.getter.api.data.ExtraData 8 | import cn.lyric.getter.api.data.LyricData 9 | import cn.lyric.getter.api.data.type.OperateType 10 | import cn.xiaowine.xkt.Tool.observableChange 11 | import com.github.kyuubiran.ezxhelper.Log 12 | import statusbar.finder.BuildConfig 13 | 14 | /** 15 | * LyricGetterExt - statusbar.finder.hook.tool 16 | * @description 参考自 https://github.com/xiaowine/Lyric-Getter/blob/master/app/src/main/kotlin/cn/lyric/getter/tool/EventTools.kt 17 | * @author VictorModi 18 | * @email victormodi@outlook.com 19 | * @date 2025/2/17 00:09 20 | */ 21 | 22 | @SuppressLint("StaticFieldLeak", "MissingPermission") 23 | object EventTool { 24 | private lateinit var context: Context 25 | private lateinit var user: UserHandle 26 | private var lastLyricData: LyricData? by observableChange(null) { _, _, newValue -> 27 | newValue?.run { 28 | if (lyric.isBlank()) { 29 | cleanLyric() 30 | } else { 31 | context.sendBroadcastAsUser(Intent().apply { 32 | action = "Lyric_Data" 33 | putExtra("Data", newValue) 34 | }, UserHandle.getUserHandleForUid(android.os.Process.myUid())) 35 | Log.d(this.toString()) 36 | } 37 | } 38 | } 39 | 40 | fun sendLyric(lyric: String, extra: ExtraData) { 41 | val refinedLyric = lyric.trim() 42 | if (refinedLyric.isBlank()) return 43 | lastLyricData = LyricData().apply { 44 | this.type = OperateType.UPDATE 45 | this.lyric = refinedLyric 46 | this.extraData = extra 47 | } 48 | } 49 | 50 | fun cleanLyric() { 51 | context.sendBroadcastAsUser(Intent().apply { 52 | action = "Lyric_Data" 53 | val lyricData = LyricData().apply { 54 | this.type = OperateType.STOP 55 | this.extraData.mergeExtra(ExtraData().apply { 56 | this.packageName = BuildConfig.APPLICATION_ID 57 | }) 58 | } 59 | putExtra("Data", lyricData) 60 | }, UserHandle.getUserHandleForUid(android.os.Process.myUid())) 61 | } 62 | 63 | fun setContext(context: Context, user: UserHandle) { 64 | this.context = context 65 | this.user = user 66 | Log.i("${BuildConfig.APPLICATION_ID} Initializing EventTool, Context by ${this.context.packageName}") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/hook/tool/Tool.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.hook.tool 2 | 3 | /** 4 | * LyricGetterExt - statusbar.finder.hook.tool 5 | * @description TODO: coming soon. 6 | * @author VictorModi 7 | * @email victormodi@outlook.com 8 | * @date 2025/2/19 13:19 9 | */ 10 | object Tool { 11 | var xpActivation: Boolean = false 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/misc/Constants.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.misc; 2 | 3 | import statusbar.finder.BuildConfig; 4 | 5 | public class Constants { 6 | public static final int NOTIFICATION_ID_LRC = 1; 7 | public static final int MSG_LYRIC_UPDATE_DONE = 2; 8 | 9 | public static final String PREFERENCE_KEY_ENABLED = "enabled"; 10 | public static final String PREFERENCE_KEY_CONNECTION_STATUS = "connection_status"; 11 | public static final String PREFERENCE_KEY_TRANSLATE_TYPE = "translate_type"; 12 | public static final String PREFERENCE_KEY_ABOUT = "about"; 13 | public static final String PREFERENCE_KEY_TARGET_PACKAGES = "target_packages"; 14 | public static final String PREFERENCE_KEY_FORCE_REPEAT = "force_repeat"; 15 | public static final String PREFERENCE_KEY_LYRICS_CONFIGURATION = "lyrics_configuration"; 16 | 17 | public static final String NOTIFICATION_CHANNEL_LRC = "lrc"; 18 | 19 | public static final int FLAG_ALWAYS_SHOW_TICKER = 0x1000000; 20 | public static final int FLAG_ONLY_UPDATE_TICKER = 0x2000000; 21 | 22 | public static final String SETTINGS_ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners"; 23 | 24 | 25 | public static final String BROADCAST_LYRICS_CHANGED = String.format("%s.LYRICS_CHANGED", BuildConfig.APPLICATION_ID); 26 | public static final String BROADCAST_LYRIC_SENTENCE_UPDATE = String.format("%s.LYRICS_SENTENCE_UPDATED", BuildConfig.APPLICATION_ID); 27 | 28 | public static final String BROADCAST_LYRICS_CHANGED_REQUEST = String.format("%s.LYRICS_CHANGED_REQUEST", BuildConfig.APPLICATION_ID); 29 | public static final String BROADCAST_LYRICS_OFFSET_UPDATE_REQUEST = String.format("%s.LYRICS_OFFSET_UPDATE_REQUEST", BuildConfig.APPLICATION_ID); 30 | public static final String BROADCAST_LYRICS_ACTIVE_UPDATE_REQUEST = String.format("%s.LYRICS_ACTIVE_UPDATE_REQUEST", BuildConfig.APPLICATION_ID); 31 | public static final String BROADCAST_LYRICS_DELETE_RESULT_REQUEST = String.format("%s.LYRICS_DELETE_RESULT_REQUEST", BuildConfig.APPLICATION_ID); 32 | public static final String BROADCAST_LYRICS_UPDATE_ALIAS_REQUEST = String.format("%s.LYRICS_UPDATE_ALIAS_REQUEST", BuildConfig.APPLICATION_ID); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/AliasModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import statusbar.finder.data.model.MediaInfo 4 | import statusbar.finder.data.repository.AliasRepository 5 | 6 | /** 7 | * LyricGetterExt - statusbar.finder.modifier 8 | * @description TODO: coming soon. 9 | * @author VictorModi 10 | * @email victormodi@outlook.com 11 | * @date 2025/3/29 17:27 12 | */ 13 | 14 | // 这样写代码脑袋会变得平平的 15 | class AliasModifier : Modifier { 16 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo? { 17 | val alias = AliasRepository.getAlias(originId) ?: return null 18 | return mediaInfo.copy( 19 | title = alias.title ?: mediaInfo.title, 20 | artist = alias.artist ?: mediaInfo.artist, 21 | album = alias.album ?: mediaInfo.album 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/HiraganaModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import com.moji4j.MojiConverter 4 | import statusbar.finder.data.model.MediaInfo 5 | import statusbar.finder.utils.CheckLanguageUtil.isLatin 6 | import statusbar.finder.utils.CheckLanguageUtil.isNoJapaneseButLatin 7 | import statusbar.finder.utils.LyricSearchUtil.getSearchKey 8 | 9 | /** 10 | * LyricGetterExt - statusbar.finder.modifiers 11 | * @description TODO: coming soon. 12 | * @author VictorModi 13 | * @email victormodi@outlook.com 14 | * @date 2025/3/13 17:41 15 | */ 16 | class HiraganaModifier : Modifier { 17 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo? { 18 | if (isNoJapaneseButLatin(mediaInfo)) return null 19 | val converter = MojiConverter() 20 | val convertedMediaInfo = mediaInfo.copy( 21 | title = converter.convertRomajiToHiragana(mediaInfo.title), 22 | artist = converter.convertRomajiToHiragana(mediaInfo.artist), 23 | album = converter.convertRomajiToHiragana(mediaInfo.album) 24 | ) 25 | if (isLatin(getSearchKey(mediaInfo))) return null 26 | return convertedMediaInfo 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/KatakanaModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import com.moji4j.MojiConverter 4 | import statusbar.finder.data.model.MediaInfo 5 | import statusbar.finder.utils.CheckLanguageUtil.isLatin 6 | import statusbar.finder.utils.CheckLanguageUtil.isNoJapaneseButLatin 7 | import statusbar.finder.utils.LyricSearchUtil.getSearchKey 8 | 9 | /** 10 | * LyricGetterExt - statusbar.finder.modifiers 11 | * @description TODO: coming soon. 12 | * @author VictorModi 13 | * @email victormodi@outlook.com 14 | * @date 2025/3/13 17:42 15 | */ 16 | class KatakanaModifier : Modifier { 17 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo? { 18 | if (isNoJapaneseButLatin(mediaInfo)) return null 19 | val converter = MojiConverter() 20 | val convertedMediaInfo = mediaInfo.copy( 21 | title = converter.convertRomajiToKatakana(mediaInfo.title), 22 | artist = converter.convertRomajiToKatakana(mediaInfo.artist), 23 | album = converter.convertRomajiToKatakana(mediaInfo.album) 24 | ) 25 | if (isLatin(getSearchKey(mediaInfo))) return null 26 | return convertedMediaInfo 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/Modifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import statusbar.finder.data.model.MediaInfo 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/3/13 17:38 11 | */ 12 | interface Modifier { 13 | fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo? 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/OriginalModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import statusbar.finder.data.model.MediaInfo 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder.modifiers 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/3/13 17:40 11 | */ 12 | class OriginalModifier : Modifier { 13 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo { 14 | return mediaInfo 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/RemoveParenthesesModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import com.github.kyuubiran.ezxhelper.Log 4 | import statusbar.finder.BuildConfig 5 | import statusbar.finder.data.model.MediaInfo 6 | 7 | /** 8 | * LyricGetterExt - statusbar.finder.modifier 9 | * @description TODO: coming soon. 10 | * @author VictorModi 11 | * @email victormodi@outlook.com 12 | * @date 2025/3/24 21:47 13 | */ 14 | class RemoveParenthesesModifier : Modifier { 15 | private fun removeBracketsAndContent(str: String): String { 16 | var result = str 17 | while (result.contains("(") || result.contains("(")) { 18 | result = result.replace(Regex("\\([^()]*\\)|([^()]*)"), "") 19 | } 20 | return result.replace("\\s+".toRegex(), " ").trim() 21 | } 22 | 23 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo { 24 | Log.d("${BuildConfig.APPLICATION_ID}::removeParenthesesModifier $mediaInfo") 25 | Log.d("${BuildConfig.APPLICATION_ID}::removeParenthesesModifier ${mediaInfo.title} -> ${removeBracketsAndContent(mediaInfo.title)}") 26 | return mediaInfo.copy( 27 | title = removeBracketsAndContent(mediaInfo.title) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/modifier/SimplifiedModifier.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.modifier 2 | 3 | import statusbar.finder.data.model.MediaInfo 4 | 5 | /** 6 | * LyricGetterExt - statusbar.finder.modifiers 7 | * @description TODO: coming soon. 8 | * @author VictorModi 9 | * @email victormodi@outlook.com 10 | * @date 2025/3/13 17:42 11 | */ 12 | class SimplifiedModifier : Modifier { 13 | override fun modify(mediaInfo: MediaInfo, originId: Long): MediaInfo { 14 | return mediaInfo.toSimple() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/provider/ILrcProvider.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.provider; 2 | 3 | import android.media.MediaMetadata; 4 | import statusbar.finder.data.model.LyricResult; 5 | import statusbar.finder.data.model.MediaInfo; 6 | 7 | import java.io.IOException; 8 | 9 | public interface ILrcProvider { 10 | @Deprecated LyricResult getLyric(MediaMetadata data) throws IOException; 11 | LyricResult getLyric(MediaInfo mediaInfo) throws IOException; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/provider/KugouProvider.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.provider; 2 | 3 | 4 | import android.media.MediaMetadata; 5 | import android.util.Base64; 6 | import android.util.Pair; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import org.json.JSONObject; 10 | import statusbar.finder.data.model.LyricResult; 11 | import statusbar.finder.data.model.MediaInfo; 12 | import statusbar.finder.utils.HttpRequestUtil; 13 | import statusbar.finder.utils.LyricSearchUtil; 14 | 15 | import java.io.IOException; 16 | import java.util.Locale; 17 | 18 | import static org.apache.commons.text.StringEscapeUtils.unescapeHtml4; 19 | 20 | 21 | public class KugouProvider implements ILrcProvider { 22 | 23 | private static final String KUGOU_BASE_URL = "https://lyrics.kugou.com/"; 24 | private static final String KUGOU_SEARCH_URL_FORMAT = KUGOU_BASE_URL + "search?ver=1&man=yes&client=pc&keyword=%s&duration=%d"; 25 | private static final String KUGOU_LRC_URL_FORMAT = KUGOU_BASE_URL + "download?ver=1&client=pc&id=%d&accesskey=%s&fmt=lrc&charset=utf8"; 26 | 27 | @Override 28 | public LyricResult getLyric(MediaMetadata data) throws IOException { 29 | return getLyric(new MediaInfo(data)); 30 | } 31 | 32 | @Override 33 | public LyricResult getLyric(MediaInfo mediaInfo) throws IOException { 34 | String searchUrl = String.format(Locale.getDefault(), KUGOU_SEARCH_URL_FORMAT, LyricSearchUtil.getSearchKey(mediaInfo), mediaInfo.getDuration()); 35 | JSONObject searchResult; 36 | try { 37 | searchResult = HttpRequestUtil.INSTANCE.getJsonResponse(searchUrl); 38 | if (searchResult != null && searchResult.getLong("status") == 200) { 39 | JSONArray array = searchResult.getJSONArray("candidates"); 40 | Pair pair = getLrcUrl(array, mediaInfo); 41 | if(pair != null){ 42 | JSONObject lrcJson = HttpRequestUtil.INSTANCE.getJsonResponse(pair.first); 43 | if (lrcJson == null) { 44 | return null; 45 | } 46 | LyricResult result = new LyricResult(); 47 | result.setLyric(unescapeHtml4(new String(Base64.decode(lrcJson.getString("content").getBytes(), Base64.DEFAULT)))); 48 | result.setDistance(pair.second.getDistance()); 49 | result.setResultInfo(pair.second); 50 | return result; 51 | } else { 52 | return null; 53 | } 54 | } 55 | } catch (JSONException e) { 56 | e.fillInStackTrace(); 57 | return null; 58 | } 59 | return null; 60 | } 61 | 62 | private static Pair getLrcUrl(JSONArray jsonArray, MediaInfo mediaInfo) throws JSONException { 63 | return getLrcUrl(jsonArray, mediaInfo.getTitle(), mediaInfo.getArtist(), mediaInfo.getAlbum()); 64 | } 65 | 66 | private static Pair getLrcUrl(JSONArray jsonArray, String songTitle, String songArtist, String songAlbum) throws JSONException { 67 | String currentAccessKey = ""; 68 | long minDistance = 10000; 69 | long currentId = -1; 70 | String resultSoundName = null; 71 | String resultArtist = null; 72 | for (int i = 0; i < jsonArray.length(); i++) { 73 | JSONObject jsonObject = jsonArray.getJSONObject(i); 74 | String soundName = jsonObject.getString("song"); 75 | String artist = jsonObject.getString("singer"); 76 | long dis = LyricSearchUtil.calculateSongInfoDistance(songTitle, songArtist, songAlbum, soundName, artist, null); 77 | if (dis < minDistance) { 78 | minDistance = dis; 79 | currentId = jsonObject.getLong("id"); 80 | currentAccessKey = jsonObject.getString("accesskey"); 81 | resultSoundName = soundName; 82 | resultArtist = artist; 83 | } 84 | } 85 | if (currentId == -1) { 86 | return null; 87 | } 88 | return new Pair<>(String.format(Locale.getDefault(), KUGOU_LRC_URL_FORMAT, currentId, currentAccessKey), new MediaInfo(resultSoundName, resultArtist, "", -1, minDistance)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/provider/NeteaseProvider.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.provider; 2 | 3 | import android.media.MediaMetadata; 4 | import android.util.Log; 5 | import android.util.Pair; 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | import statusbar.finder.data.model.LyricResult; 10 | import statusbar.finder.data.model.MediaInfo; 11 | import statusbar.finder.utils.HttpRequestUtil; 12 | import statusbar.finder.utils.LyricSearchUtil; 13 | 14 | import java.io.IOException; 15 | import java.util.Locale; 16 | 17 | public class NeteaseProvider implements ILrcProvider { 18 | 19 | private static final String NETEASE_BASE_URL = "https://music.163.com/api/"; 20 | 21 | private static final String NETEASE_SEARCH_URL_FORMAT = NETEASE_BASE_URL + "search/get?s=%s&type=1&offset=0&limit=5"; 22 | private static final String NETEASE_LRC_URL_FORMAT = NETEASE_BASE_URL + "song/lyric?os=pc&id=%d&lv=-1&kv=-1&tv=-1"; 23 | 24 | @Override 25 | public LyricResult getLyric(MediaMetadata data) throws IOException { 26 | return getLyric(new MediaInfo(data)); 27 | } 28 | 29 | @Override 30 | public LyricResult getLyric(MediaInfo mediaInfo) throws IOException { 31 | String searchUrl = String.format(NETEASE_SEARCH_URL_FORMAT, LyricSearchUtil.getSearchKey(mediaInfo)); 32 | Log.d("searchUrl", searchUrl); 33 | JSONObject searchResult; 34 | try { 35 | searchResult = HttpRequestUtil.INSTANCE.getJsonResponse(searchUrl); 36 | if (searchResult != null && searchResult.getLong("code") == 200) { 37 | JSONArray array = searchResult.getJSONObject("result").getJSONArray("songs"); 38 | Pair pair = getLrcUrl(array, mediaInfo); 39 | if (pair != null) { 40 | JSONObject lrcJson = HttpRequestUtil.INSTANCE.getJsonResponse(pair.first); 41 | if (lrcJson == null) { 42 | return null; 43 | } 44 | LyricResult result = new LyricResult(); 45 | result.setLyric(lrcJson.getJSONObject("lrc").getString("lyric")); 46 | try { 47 | result.setTranslatedLyric(lrcJson.getJSONObject("tlyric").getString("lyric")); 48 | } catch (JSONException e) { 49 | result.setTranslatedLyric(null); 50 | } 51 | result.setDistance(pair.second.getDistance()); 52 | result.setResultInfo(pair.second); 53 | return result; 54 | } else { 55 | return null; 56 | } 57 | } 58 | } catch (JSONException e) { 59 | e.fillInStackTrace(); 60 | return null; 61 | } 62 | return null; 63 | } 64 | 65 | private static Pair getLrcUrl(JSONArray jsonArray, MediaInfo mediaInfo) throws JSONException { 66 | return getLrcUrl(jsonArray, mediaInfo.getTitle(), mediaInfo.getArtist(), mediaInfo.getAlbum()); 67 | } 68 | 69 | private static Pair getLrcUrl(JSONArray jsonArray, String songTitle, String songArtist, String songAlbum) throws JSONException { 70 | long currentID = -1; 71 | long minDistance = 10000; 72 | String resultSoundName = null; 73 | JSONArray resultArtists = null; 74 | String resultAlbumName = null; 75 | for (int i = 0; i < jsonArray.length(); i++) { 76 | JSONObject jsonObject = jsonArray.getJSONObject(i); 77 | String soundName = jsonObject.getString("name"); 78 | JSONArray artists = jsonObject.getJSONArray("artists"); 79 | String albumName = jsonObject.getJSONObject("album").getString("name"); 80 | long dis = LyricSearchUtil.calculateSongInfoDistance(songTitle, songArtist, songAlbum, soundName, LyricSearchUtil.parseArtists(artists, "name"), albumName); 81 | if (dis <= minDistance) { 82 | minDistance = dis; 83 | currentID = jsonObject.getLong("id"); 84 | resultSoundName = soundName; 85 | resultArtists = artists; 86 | resultAlbumName = albumName; 87 | } 88 | } 89 | if (currentID == -1) { 90 | return null; 91 | } 92 | return new Pair<>(String.format(Locale.getDefault(), NETEASE_LRC_URL_FORMAT, currentID), new MediaInfo(resultSoundName, LyricSearchUtil.parseArtists(resultArtists, "name"), resultAlbumName, -1, minDistance)); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/provider/QQMusicProvider.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.provider; 2 | 3 | import android.media.MediaMetadata; 4 | import android.util.Base64; 5 | import android.util.Pair; 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | import statusbar.finder.data.model.LyricResult; 10 | import statusbar.finder.data.model.MediaInfo; 11 | import statusbar.finder.utils.HttpRequestUtil; 12 | import statusbar.finder.utils.LyricSearchUtil; 13 | 14 | import java.io.IOException; 15 | import java.util.Locale; 16 | 17 | import static org.apache.commons.text.StringEscapeUtils.unescapeHtml4; 18 | 19 | public class QQMusicProvider implements ILrcProvider { 20 | private static final String QM_BASE_URL = "https://c.y.qq.com/"; 21 | private static final String QM_REFERER = "https://y.qq.com"; 22 | private static final String QM_SEARCH_URL_FORMAT = QM_BASE_URL + "soso/fcgi-bin/client_search_cp?w=%s&format=json"; 23 | private static final String QM_LRC_URL_FORMAT = QM_BASE_URL + "lyric/fcgi-bin/fcg_query_lyric_yqq.fcg?songmid=%s&format=json"; 24 | 25 | @Override 26 | public LyricResult getLyric(MediaMetadata data) throws IOException { 27 | return getLyric(new MediaInfo(data)); 28 | } 29 | 30 | @Override 31 | public LyricResult getLyric(MediaInfo mediaInfo) throws IOException { 32 | String searchUrl = String.format(Locale.getDefault(), QM_SEARCH_URL_FORMAT, LyricSearchUtil.getSearchKey(mediaInfo)); 33 | JSONObject searchResult; 34 | try { 35 | searchResult = HttpRequestUtil.INSTANCE.getJsonResponse(searchUrl, QM_REFERER); 36 | if (searchResult != null && searchResult.getLong("code") == 0) { 37 | JSONArray array = searchResult.getJSONObject("data").getJSONObject("song").getJSONArray("list"); 38 | Pair pair = getLrcUrl(array, mediaInfo); 39 | if (pair != null) { 40 | JSONObject lrcJson = HttpRequestUtil.INSTANCE.getJsonResponse(pair.first, QM_REFERER); 41 | if (lrcJson == null) { 42 | return null; 43 | } 44 | LyricResult result = new LyricResult(); 45 | result.setLyric(unescapeHtml4(new String(Base64.decode(lrcJson.getString("lyric").getBytes(), Base64.DEFAULT)))); 46 | result.setDistance(pair.second.getDistance()); 47 | result.setResultInfo(pair.second); 48 | return result; 49 | } else { 50 | return null; 51 | } 52 | } 53 | } catch (JSONException e) { 54 | e.fillInStackTrace(); 55 | return null; 56 | } 57 | return null; 58 | } 59 | 60 | private static Pair getLrcUrl(JSONArray jsonArray, MediaInfo mediaInfo) throws JSONException { 61 | return getLrcUrl(jsonArray, mediaInfo.getTitle(), mediaInfo.getArtist(), mediaInfo.getAlbum()); 62 | } 63 | 64 | private static Pair getLrcUrl(JSONArray jsonArray, String songTitle, String songArtist, String songAlbum) throws JSONException { 65 | String currentMID = ""; 66 | long minDistance = 10000; 67 | String resultSoundName = null; 68 | String resultAlbumName = null; 69 | JSONArray resultSingers = null; 70 | for (int i = 0; i < jsonArray.length(); i++) { 71 | JSONObject jsonObject = jsonArray.getJSONObject(i); 72 | String soundName = jsonObject.getString("songname"); 73 | String albumName = jsonObject.getString("albumname"); 74 | JSONArray singers = jsonObject.getJSONArray("singer"); 75 | long dis = LyricSearchUtil.calculateSongInfoDistance(songTitle, songArtist, songAlbum, soundName, LyricSearchUtil.parseArtists(singers, "name"), albumName); 76 | if (dis < minDistance) { 77 | minDistance = dis; 78 | resultSoundName = soundName; 79 | resultAlbumName = albumName; 80 | resultSingers = singers; 81 | currentMID = jsonObject.getString("songmid"); 82 | } 83 | } 84 | if (currentMID.isEmpty()) {return null;} 85 | return new Pair<>(String.format(Locale.getDefault(), QM_LRC_URL_FORMAT, currentMID), new MediaInfo(resultSoundName, LyricSearchUtil.parseArtists(resultSingers, "name"), resultAlbumName, -1, minDistance)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/utils/CheckLanguageUtil.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.utils; 2 | 3 | import com.moji4j.MojiDetector; 4 | import statusbar.finder.data.model.MediaInfo; 5 | 6 | import static statusbar.finder.utils.LyricSearchUtil.getSearchKey; 7 | 8 | public class CheckLanguageUtil { 9 | 10 | private static MojiDetector detector; 11 | // public static boolean isJapenese(String text) { 12 | // Set japaneseUnicodeBlocks = new HashSet() {{ 13 | // add(Character.UnicodeBlock.HIRAGANA); 14 | // add(Character.UnicodeBlock.KATAKANA); 15 | // add(Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS); 16 | // }}; 17 | // 18 | // for (char c : text.toCharArray()) { 19 | // if (japaneseUnicodeBlocks.contains(Character.UnicodeBlock.of(c))) { 20 | // return true; 21 | // } else 22 | // return false; 23 | // } 24 | // return false; 25 | // } 26 | 27 | private static MojiDetector getDetector() { 28 | return detector != null ? detector : (detector = new MojiDetector()); 29 | } 30 | 31 | public static boolean isJapanese(String str) { 32 | return getDetector().hasKana(str) || getDetector().hasKanji(str); 33 | } 34 | 35 | public static boolean isLatin(String str) { 36 | return getDetector().hasLatin(str); 37 | } 38 | 39 | public static boolean isNoJapaneseButLatin(MediaInfo mediaInfo) { 40 | String searchKey = getSearchKey(mediaInfo); 41 | return !isJapanese(searchKey) && isLatin(searchKey); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/utils/HttpRequestUtil.kt: -------------------------------------------------------------------------------- 1 | package statusbar.finder.utils 2 | 3 | /** 4 | * LyricGetterExt - statusbar.finder.utils 5 | * @description TODO: coming soon. 6 | * @author VictorModi 7 | * @email victormodi@outlook.com 8 | * @date 2025/2/21 09:56 9 | */ 10 | 11 | import org.json.JSONObject 12 | import java.io.ByteArrayOutputStream 13 | import java.io.IOException 14 | import java.io.InputStream 15 | import java.net.CookieHandler 16 | import java.net.CookieManager 17 | import java.net.HttpURLConnection 18 | import java.net.URL 19 | 20 | object HttpRequestUtil { 21 | // 初始化默认 Cookie 管理器 22 | init { 23 | CookieHandler.setDefault(CookieManager()) 24 | } 25 | 26 | /** 27 | * 获取指定URL的JSON响应。 28 | * 29 | * @param url 请求的URL 30 | * @return 返回从URL获取的JSON响应的JSONObject对象 31 | * @throws IOException 如果发生I/O错误 32 | */ 33 | fun getJsonResponse(url: String): JSONObject? { 34 | return getJsonResponse(url, null) 35 | } 36 | 37 | /** 38 | * 获取指定URL的JSON响应。 39 | * 40 | * @param url 请求的URL 41 | * @param referer 请求的引用页 42 | * @return 返回从URL获取的JSON响应的JSONObject对象 43 | * @throws IOException 如果发生I/O错误 44 | */ 45 | fun getJsonResponse(url: String, referer: String?): JSONObject? { 46 | var connection: HttpURLConnection = createHttpURLConnection(url, referer) 47 | 48 | do { 49 | val requestUrl = connection.getHeaderField("Location") ?: url 50 | connection = createHttpURLConnection(requestUrl, referer) 51 | } while (connection.responseCode == 301 || connection.responseCode == 302) 52 | 53 | return convertStreamToJSONObject(connection) 54 | } 55 | 56 | /** 57 | * 创建并返回一个HTTP连接对象。 58 | * 59 | * @param url 请求的URL 60 | * @param referer 请求的引用页 61 | * @return 返回一个HttpURLConnection对象 62 | * @throws IOException 如果发生I/O错误 63 | */ 64 | private fun createHttpURLConnection(url: String, referer: String?): HttpURLConnection { 65 | val connection = (URL(url).openConnection() as HttpURLConnection).apply { 66 | requestMethod = "GET" 67 | setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:77.0) Gecko/20100101 Firefox/77.0") 68 | referer?.let { 69 | setRequestProperty("Referer", it) 70 | } 71 | connectTimeout = 5000 72 | readTimeout = 5000 73 | connect() 74 | } 75 | return connection 76 | } 77 | 78 | /** 79 | * 读取输入流并转换为JSON对象 80 | */ 81 | 82 | private fun convertStreamToJSONObject(connection: HttpURLConnection): JSONObject? { 83 | return connection.inputStream.use { inputStream -> 84 | val data = inputStream.readStream() 85 | try { 86 | JSONObject(String(data)) 87 | } catch (e: Exception) { 88 | e.printStackTrace() 89 | null 90 | } finally { 91 | connection.disconnect() 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * 扩展函数:将 InputStream 读取为 ByteArray 98 | */ 99 | private fun InputStream.readStream(): ByteArray { 100 | return use { input -> 101 | ByteArrayOutputStream().use { output -> 102 | val buffer = ByteArray(1024) 103 | var length: Int 104 | while (input.read(buffer).also { length = it } != -1) { 105 | output.write(buffer, 0, length) 106 | } 107 | output.toByteArray() 108 | } 109 | } 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/utils/LyricSearchUtil.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.utils; 2 | 3 | import android.media.MediaMetadata; 4 | import android.os.Build; 5 | import android.text.TextUtils; 6 | import com.github.houbb.opencc4j.util.ZhConverterUtil; 7 | import org.json.JSONArray; 8 | import org.json.JSONException; 9 | import statusbar.finder.data.model.MediaInfo; 10 | 11 | import java.io.UnsupportedEncodingException; 12 | import java.net.URLEncoder; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.regex.Pattern; 15 | 16 | public class LyricSearchUtil { 17 | 18 | private static final Pattern LyricContentPattern = Pattern.compile("(\\[\\d\\d:\\d\\d\\.\\d{0,3}]|\\[\\d\\d:\\d\\d])[^\\r\\n]"); 19 | 20 | private static void convertIfNecessary(String input) { 21 | if (ZhConverterUtil.isTraditional(input) && !(CheckLanguageUtil.isJapanese(input))) { 22 | input = ZhConverterUtil.toSimple(input); 23 | } 24 | } 25 | 26 | public static String getSearchKey(String title, String artist, String album) { 27 | String ret; 28 | 29 | convertIfNecessary(title); 30 | convertIfNecessary(artist); 31 | convertIfNecessary(album); 32 | 33 | if (!TextUtils.isEmpty(artist)) { 34 | ret = artist + "-" + title; 35 | } else if (!TextUtils.isEmpty(album)) { 36 | ret = album + "-" + title; 37 | } else { 38 | ret = title; 39 | } 40 | if (!TextUtils.isEmpty(ret)) { 41 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 42 | return URLEncoder.encode(ret, StandardCharsets.UTF_8); 43 | } 44 | try { 45 | return URLEncoder.encode(ret, "UTF-8"); 46 | } catch (UnsupportedEncodingException e) { 47 | return ret; 48 | } 49 | } else { 50 | return ret; 51 | } 52 | } 53 | 54 | @Deprecated public static String getSearchKey(MediaMetadata metadata) { 55 | return getSearchKey(metadata.getString(MediaMetadata.METADATA_KEY_TITLE), metadata.getString(MediaMetadata.METADATA_KEY_ALBUM), metadata.getString(MediaMetadata.METADATA_KEY_ARTIST)); 56 | } 57 | 58 | public static String getSearchKey(MediaInfo mediaInfo) { 59 | return getSearchKey(mediaInfo.getTitle(), mediaInfo.getArtist(), mediaInfo.getAlbum()); 60 | } 61 | 62 | public static String parseArtists(JSONArray jsonArray, String key) { 63 | try { 64 | StringBuilder stringBuilder = new StringBuilder(); 65 | for (int i = 0; i < jsonArray.length(); i++) { 66 | stringBuilder.append(jsonArray.getJSONObject(i).getString(key)); 67 | if (i < jsonArray.length() - 1) stringBuilder.append('/'); 68 | } 69 | return stringBuilder.toString(); 70 | } catch (JSONException e) { 71 | e.fillInStackTrace(); 72 | } 73 | return ""; 74 | } 75 | 76 | public static long calculateSongInfoDistance(String realTitle, String realArtist, String realAlbum, String title, String artist, String album) { 77 | if (ZhConverterUtil.isTraditional(realTitle)) { 78 | if (!(CheckLanguageUtil.isJapanese(realTitle))) 79 | realTitle = ZhConverterUtil.toSimple(realTitle); 80 | } 81 | if (ZhConverterUtil.isTraditional(realArtist)) { 82 | if (!(CheckLanguageUtil.isJapanese(realArtist))) 83 | realArtist = ZhConverterUtil.toSimple(realArtist); 84 | } 85 | if (ZhConverterUtil.isTraditional(realAlbum)) { 86 | if (!(CheckLanguageUtil.isJapanese(realAlbum))) 87 | realAlbum = ZhConverterUtil.toSimple(realAlbum); 88 | } 89 | 90 | if (!realTitle.contains(title) && !title.contains(realTitle) || TextUtils.isEmpty(title)) { 91 | return 10000; 92 | } 93 | long res = levenshtein(title, realTitle) * 100L; 94 | res += levenshtein(artist, realArtist) * 10L; 95 | res += levenshtein(album, realAlbum); 96 | return res; 97 | } 98 | 99 | public static long calculateSongInfoDistance(MediaInfo mediaInfo, String title, String artist, String album) { 100 | return calculateSongInfoDistance( 101 | mediaInfo.getTitle(), 102 | mediaInfo.getArtist(), 103 | mediaInfo.getAlbum(), title, artist, album); 104 | } 105 | 106 | @Deprecated public static long calculateSongInfoDistance(MediaMetadata metadata, String title, String artist, String album) { 107 | return calculateSongInfoDistance( 108 | metadata.getString(MediaMetadata.METADATA_KEY_TITLE), 109 | metadata.getString(MediaMetadata.METADATA_KEY_ARTIST), 110 | metadata.getString(MediaMetadata.METADATA_KEY_ALBUM), 111 | title, 112 | artist, 113 | album 114 | ); 115 | } 116 | 117 | public static int levenshtein(CharSequence a, CharSequence b) { 118 | if (TextUtils.isEmpty(a)) { 119 | return (b == null) ? 0 : b.length(); 120 | } else if (TextUtils.isEmpty(b)) { 121 | return a.length(); 122 | } 123 | final int lenA = a.length(), lenB = b.length(); 124 | int[][] dp = new int[lenA + 1][lenB + 1]; 125 | for (int i = 0; i <= lenA; i++) dp[i][0] = i; 126 | for (int j = 0; j <= lenB; j++) dp[0][j] = j; 127 | for (int i = 1; i <= lenA; i++) { 128 | for (int j = 1; j <= lenB; j++) { 129 | int flag = (a.charAt(i - 1) == b.charAt(j - 1)) ? 0 : 1; 130 | dp[i][j] = Math.min(dp[i - 1][j - 1] + flag, 131 | Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); 132 | } 133 | } 134 | return dp[lenA][lenB]; 135 | } 136 | 137 | public static boolean isLyricContent(String content) { 138 | if (TextUtils.isEmpty(content)) return false; 139 | return LyricContentPattern.matcher(content).find(); 140 | } 141 | 142 | public static String[] extractLyric(String lyricLine) { // 解析歌词行 [0] 时间戳 [1] 歌词文本 143 | int startIndex = lyricLine.indexOf("["); 144 | int endIndex = lyricLine.indexOf("]"); 145 | 146 | if (startIndex != -1 && endIndex != -1) { 147 | String timeStamp = lyricLine.substring(startIndex + 1, endIndex); 148 | String lyricText = lyricLine.substring(endIndex + 1).trim(); 149 | return new String[]{timeStamp, lyricText}; 150 | } 151 | 152 | return null; 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/statusbar/finder/utils/UnicodeUtil.java: -------------------------------------------------------------------------------- 1 | package statusbar.finder.utils; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | public class UnicodeUtil { 7 | // https://blog.csdn.net/JodenHe/article/details/77343197 8 | public static String unicodeStr2String(String unicodeStr) { 9 | int length = unicodeStr.length(); 10 | int count = 0; 11 | //正则匹配条件,可匹配“\\u”1到4位,一般是4位可直接使用 String regex = "\\\\u[a-f0-9A-F]{4}"; 12 | String regex = "\\\\u[a-f0-9A-F]{1,4}"; 13 | Pattern pattern = Pattern.compile(regex); 14 | Matcher matcher = pattern.matcher(unicodeStr); 15 | StringBuilder sb = new StringBuilder(); 16 | 17 | while(matcher.find()) { 18 | String oldChar = matcher.group();//原本的Unicode字符 19 | String newChar = unicode2String(oldChar);//转换为普通字符 20 | // int index = unicodeStr.indexOf(oldChar); 21 | // 在遇见重复出现的unicode代码的时候会造成从源字符串获取非unicode编码字符的时候截取索引越界等 22 | int index = matcher.start(); 23 | 24 | sb.append(unicodeStr.substring(count, index));//添加前面不是unicode的字符 25 | sb.append(newChar);//添加转换后的字符 26 | count = index+oldChar.length();//统计下标移动的位置 27 | } 28 | sb.append(unicodeStr.substring(count, length));//添加末尾不是Unicode的字符 29 | return sb.toString(); 30 | } 31 | 32 | /** 33 | * 字符串转换unicode 34 | * @param string 原始字符串 35 | * @return Unicode 字符串 36 | */ 37 | public static String string2Unicode(String string) { 38 | StringBuilder unicode = new StringBuilder(); 39 | for (int i = 0; i < string.length(); i++) { 40 | // 取出每一个字符 41 | char c = string.charAt(i); 42 | // 转换为unicode 43 | unicode.append("\\u").append(Integer.toHexString(c)); 44 | } 45 | 46 | return unicode.toString(); 47 | } 48 | 49 | /** 50 | * unicode 转字符串 51 | * @param unicode 全为 Unicode 的字符串 52 | * @return 转换后的字符串 53 | */ 54 | public static String unicode2String(String unicode) { 55 | StringBuilder string = new StringBuilder(); 56 | String[] hex = unicode.split("\\\\u"); 57 | 58 | for (int i = 1; i < hex.length; i++) { 59 | // 转换出每一个代码点 60 | int data = Integer.parseInt(hex[i], 16); 61 | // 追加成string 62 | string.append((char) data); 63 | } 64 | 65 | return string.toString(); 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/res/color-night-v31/settingslib_switch_track_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/color-v31/settingslib_surface_light.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/color-v31/settingslib_switch_thumb_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/color-v31/settingslib_switch_track_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 22 | 23 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/color-v31/settingslib_switch_track_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_add.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_statusbar_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_progress_horizontal.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_switch_bar_bg_disabled.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_switch_bar_bg_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_switch_bar_bg_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_switch_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v31/settingslib_switch_track.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_music.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout-v31/collapsing_toolbar_base_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 34 | 35 | 53 | 54 | 61 | 62 | 63 | 64 | 65 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout-v31/settingslib_main_switch_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 25 | 26 | 34 | 35 | 46 | 47 | 56 | 57 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/applist_preference_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 18 | 27 | 28 | 38 | 39 | 48 | 49 | 58 | 59 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/collapsing_toolbar_base_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 26 | 32 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_lyric.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/lrcview.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 23 | 24 | 34 | 35 | 40 | 41 | 51 | 52 | 58 | 63 | 71 | 79 | 80 | 87 | 88 | 89 | 96 | 97 | 98 | 103 | 104 | 114 | 115 | 116 |