├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── me
│ │ └── kyuubiran
│ │ └── ncmdumper
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── cpp
│ │ ├── CMakeLists.txt
│ │ └── src
│ │ │ ├── AES.cpp
│ │ │ ├── AES.h
│ │ │ ├── json.hpp
│ │ │ ├── ncm.cpp
│ │ │ ├── ncm.h
│ │ │ └── ncmdumper.cpp
│ ├── java
│ │ └── me
│ │ │ └── kyuubiran
│ │ │ └── ncmdumper
│ │ │ ├── MainActivity.kt
│ │ │ ├── MyApplication.kt
│ │ │ └── ui
│ │ │ ├── pages
│ │ │ ├── MainPage.kt
│ │ │ └── SettingsPage.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ ├── utils
│ │ │ ├── DpUtils.kt
│ │ │ └── Dumper.kt
│ │ │ └── views
│ │ │ └── NcmFileItem.kt
│ └── res
│ │ ├── drawable
│ │ ├── baseline_arrow_back_24.xml
│ │ ├── baseline_more_vert_24.xml
│ │ ├── baseline_refresh_24.xml
│ │ ├── baseline_settings_24.xml
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-zh-rCN
│ │ └── strings.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── me
│ └── kyuubiran
│ └── ncmdumper
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | /.idea/
17 | /app/release/
18 | /app/debug/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NcmDumperApp
2 | 从某数字音乐加密文件中导出mp3/flac等格式的音乐
3 | **注:一切开发旨在学习,请勿用于非法用途**
4 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | }
5 |
6 | @Suppress("UnstableApiUsage")
7 | android {
8 | namespace = "me.kyuubiran.ncmdumper"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | applicationId = "me.kyuubiran.ncmdumper"
13 | minSdk = 24
14 | targetSdk = 34
15 | versionCode = 4
16 | versionName = "0.4"
17 |
18 | buildFeatures {
19 | buildConfig = true
20 | }
21 |
22 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
23 | vectorDrawables {
24 | useSupportLibrary = true
25 | }
26 | externalNativeBuild {
27 | cmake {
28 | val flags = arrayOf(
29 | "-Wignored-attributes",
30 | )
31 |
32 | cppFlags("-std=c++2a", *flags)
33 | cFlags("-std=c18", *flags)
34 | }
35 | }
36 | }
37 |
38 | buildTypes {
39 | release {
40 | isMinifyEnabled = false
41 | proguardFiles(
42 | getDefaultProguardFile("proguard-android-optimize.txt"),
43 | "proguard-rules.pro"
44 | )
45 |
46 | externalNativeBuild {
47 | cmake {
48 | val releaseFlags = arrayOf(
49 | "-O3 -DNDEBUG"
50 | )
51 |
52 | cppFlags += releaseFlags
53 | cFlags += releaseFlags
54 | }
55 | }
56 | }
57 | }
58 | compileOptions {
59 | sourceCompatibility = JavaVersion.VERSION_11
60 | targetCompatibility = JavaVersion.VERSION_11
61 | }
62 | kotlinOptions {
63 | jvmTarget = "11"
64 | }
65 | buildFeatures {
66 | compose = true
67 | }
68 | composeOptions {
69 | kotlinCompilerExtensionVersion = "1.5.1"
70 | }
71 | packaging {
72 | resources {
73 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
74 | }
75 | }
76 | externalNativeBuild {
77 | cmake {
78 | path = file("src/main/cpp/CMakeLists.txt")
79 | version = "3.22.1"
80 | }
81 | }
82 | }
83 |
84 | dependencies {
85 | implementation(libs.androidx.core.ktx)
86 | implementation(libs.androidx.lifecycle.runtime.ktx)
87 | implementation(libs.androidx.activity.compose)
88 | implementation(platform(libs.androidx.compose.bom))
89 | implementation(libs.androidx.ui)
90 | implementation(libs.androidx.ui.graphics)
91 | implementation(libs.androidx.ui.tooling.preview)
92 | implementation(libs.androidx.material)
93 | implementation(libs.androidx.activity.compose)
94 | implementation(libs.androidx.navigation.compose)
95 |
96 | implementation(libs.accompanist.systemuicontroller)
97 |
98 | testImplementation(libs.junit)
99 | androidTestImplementation(libs.androidx.junit)
100 | androidTestImplementation(libs.androidx.espresso.core)
101 | androidTestImplementation(platform(libs.androidx.compose.bom))
102 | androidTestImplementation(libs.androidx.ui.test.junit4)
103 | debugImplementation(libs.androidx.ui.tooling)
104 | debugImplementation(libs.androidx.ui.test.manifest)
105 | }
--------------------------------------------------------------------------------
/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/androidTest/java/me/kyuubiran/ncmdumper/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("me.kyuubiran.ncmdumper", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
19 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/cpp/CMakeLists.txt:
--------------------------------------------------------------------------------
1 |
2 | # For more information about using CMake with Android Studio, read the
3 | # documentation: https://d.android.com/studio/projects/add-native-code.html.
4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples.
5 |
6 | # Sets the minimum CMake version required for this project.
7 | cmake_minimum_required(VERSION 3.22.1)
8 |
9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
10 | # Since this is the top level CMakeLists.txt, the project name is also accessible
11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
12 | # build script scope).
13 | project("ncmdumper")
14 |
15 | # Creates and names a library, sets it as either STATIC
16 | # or SHARED, and provides the relative paths to its source code.
17 | # You can define multiple libraries, and CMake builds them for you.
18 | # Gradle automatically packages shared libraries with your APK.
19 | #
20 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
21 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
22 | # is preferred for the same purpose.
23 | #
24 | # In order to load a library into your app from Java/Kotlin, you must call
25 | # System.loadLibrary() and pass the name of the library defined here;
26 | # for GameActivity/NativeActivity derived applications, the same library name must be
27 | # used in the AndroidManifest.xml file.
28 | add_library(${CMAKE_PROJECT_NAME} SHARED
29 | # List C/C++ source files with relative paths to this CMakeLists.txt.
30 | src/ncmdumper.cpp src/AES.cpp src/ncm.cpp)
31 |
32 | # Specifies libraries CMake should link to your target library. You
33 | # can link libraries from various origins, such as libraries defined in this
34 | # build script, prebuilt third-party libraries, or Android system libraries.
35 | target_link_libraries(${CMAKE_PROJECT_NAME}
36 | # List libraries link to the target library
37 | android
38 | log)
39 |
--------------------------------------------------------------------------------
/app/src/main/cpp/src/AES.cpp:
--------------------------------------------------------------------------------
1 | #include "AES.h"
2 |
3 | AES::AES(const AESKeyLength keyLength) {
4 | switch (keyLength) {
5 | case AESKeyLength::AES_128:
6 | this->Nk = 4;
7 | this->Nr = 10;
8 | break;
9 | case AESKeyLength::AES_192:
10 | this->Nk = 6;
11 | this->Nr = 12;
12 | break;
13 | case AESKeyLength::AES_256:
14 | this->Nk = 8;
15 | this->Nr = 14;
16 | break;
17 | }
18 | }
19 |
20 | std::vector AES::EncryptECB(const uint8_t in[], size_t inLen,
21 | const uint8_t key[]) {
22 | CheckLength(inLen);
23 | auto out = std::vector(inLen);
24 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
25 | KeyExpansion(key, roundKeys.data());
26 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
27 | EncryptBlock(in + i, out.data() + i, roundKeys.data());
28 | }
29 |
30 | return out;
31 | }
32 |
33 | std::vector AES::DecryptECB(const uint8_t in[], size_t inLen,
34 | const uint8_t key[]) {
35 | CheckLength(inLen);
36 | auto out = std::vector(inLen);
37 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
38 | KeyExpansion(key, roundKeys.data());
39 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
40 | DecryptBlock(in + i, out.data() + i, roundKeys.data());
41 | }
42 |
43 | return out;
44 | }
45 |
46 | std::vector AES::EncryptCBC(const uint8_t in[], size_t inLen,
47 | const uint8_t key[],
48 | const uint8_t *iv) {
49 | CheckLength(inLen);
50 | auto out = std::vector(inLen);
51 | uint8_t block[blockBytesLen];
52 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
53 | KeyExpansion(key, roundKeys.data());
54 | memcpy(block, iv, blockBytesLen);
55 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
56 | XorBlocks(block, in + i, block, blockBytesLen);
57 | EncryptBlock(block, out.data() + i, roundKeys.data());
58 | memcpy(block, out.data() + i, blockBytesLen);
59 | }
60 |
61 | return out;
62 | }
63 |
64 | std::vector AES::DecryptCBC(const uint8_t in[], size_t inLen,
65 | const uint8_t key[],
66 | const uint8_t *iv) {
67 | CheckLength(inLen);
68 | auto out = std::vector(inLen);
69 | uint8_t block[blockBytesLen];
70 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
71 | KeyExpansion(key, roundKeys.data());
72 | memcpy(block, iv, blockBytesLen);
73 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
74 | DecryptBlock(in + i, out.data() + i, roundKeys.data());
75 | XorBlocks(block, out.data() + i, out.data() + i, blockBytesLen);
76 | memcpy(block, in + i, blockBytesLen);
77 | }
78 |
79 | return out;
80 | }
81 |
82 | std::vector AES::EncryptCFB(const uint8_t in[], size_t inLen,
83 | const uint8_t key[],
84 | const uint8_t *iv) {
85 | CheckLength(inLen);
86 | auto out = std::vector(inLen);
87 | uint8_t block[blockBytesLen];
88 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
89 | KeyExpansion(key, roundKeys.data());
90 | memcpy(block, iv, blockBytesLen);
91 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
92 | uint8_t encryptedBlock[blockBytesLen];
93 | EncryptBlock(block, encryptedBlock, roundKeys.data());
94 | XorBlocks(in + i, encryptedBlock, out.data() + i, blockBytesLen);
95 | memcpy(block, out.data() + i, blockBytesLen);
96 | }
97 |
98 | return out;
99 | }
100 |
101 | std::vector AES::DecryptCFB(const uint8_t in[], size_t inLen,
102 | const uint8_t key[],
103 | const uint8_t *iv) {
104 | CheckLength(inLen);
105 | auto out = std::vector(inLen);
106 | uint8_t block[blockBytesLen];
107 | auto roundKeys = std::vector(4 * Nb * (Nr + 1));
108 | KeyExpansion(key, roundKeys.data());
109 | memcpy(block, iv, blockBytesLen);
110 | for (size_t i = 0; i < inLen; i += blockBytesLen) {
111 | uint8_t encryptedBlock[blockBytesLen];
112 | EncryptBlock(block, encryptedBlock, roundKeys.data());
113 | XorBlocks(in + i, encryptedBlock, out.data() + i, blockBytesLen);
114 | memcpy(block, in + i, blockBytesLen);
115 | }
116 |
117 | return out;
118 | }
119 |
120 | void AES::CheckLength(const size_t len) {
121 | if (len % blockBytesLen != 0) {
122 | throw std::length_error("Plaintext length must be divisible by " +
123 | std::to_string(blockBytesLen));
124 | }
125 | }
126 |
127 | void AES::EncryptBlock(const uint8_t in[], uint8_t out[], uint8_t *roundKeys) const {
128 | uint8_t state[4][Nb];
129 | size_t i, j, round;
130 |
131 | for (i = 0; i < 4; i++) {
132 | for (j = 0; j < Nb; j++) {
133 | state[i][j] = in[i + 4 * j];
134 | }
135 | }
136 |
137 | AddRoundKey(state, roundKeys);
138 |
139 | for (round = 1; round <= Nr - 1; round++) {
140 | SubBytes(state);
141 | ShiftRows(state);
142 | MixColumns(state);
143 | AddRoundKey(state, roundKeys + round * 4 * Nb);
144 | }
145 |
146 | SubBytes(state);
147 | ShiftRows(state);
148 | AddRoundKey(state, roundKeys + Nr * 4 * Nb);
149 |
150 | for (i = 0; i < 4; i++) {
151 | for (j = 0; j < Nb; j++) {
152 | out[i + 4 * j] = state[i][j];
153 | }
154 | }
155 | }
156 |
157 | void AES::DecryptBlock(const uint8_t in[], uint8_t out[],
158 | uint8_t *roundKeys) const {
159 | uint8_t state[4][Nb];
160 | size_t i, j, round;
161 |
162 | for (i = 0; i < 4; i++) {
163 | for (j = 0; j < Nb; j++) {
164 | state[i][j] = in[i + 4 * j];
165 | }
166 | }
167 |
168 | AddRoundKey(state, roundKeys + Nr * 4 * Nb);
169 |
170 | for (round = Nr - 1; round >= 1; round--) {
171 | InvSubBytes(state);
172 | InvShiftRows(state);
173 | AddRoundKey(state, roundKeys + round * 4 * Nb);
174 | InvMixColumns(state);
175 | }
176 |
177 | InvSubBytes(state);
178 | InvShiftRows(state);
179 | AddRoundKey(state, roundKeys);
180 |
181 | for (i = 0; i < 4; i++) {
182 | for (j = 0; j < Nb; j++) {
183 | out[i + 4 * j] = state[i][j];
184 | }
185 | }
186 | }
187 |
188 | void AES::SubBytes(uint8_t state[4][Nb]) {
189 | size_t i, j;
190 | uint8_t t;
191 | for (i = 0; i < 4; i++) {
192 | for (j = 0; j < Nb; j++) {
193 | t = state[i][j];
194 | state[i][j] = sbox[t / 16][t % 16];
195 | }
196 | }
197 | }
198 |
199 | void AES::ShiftRow(uint8_t state[4][Nb], size_t i,
200 | size_t n) // shift row i on n positions
201 | {
202 | uint8_t tmp[Nb];
203 | for (size_t j = 0; j < Nb; j++) {
204 | tmp[j] = state[i][(j + n) % Nb];
205 | }
206 | memcpy(state[i], tmp, Nb * sizeof(uint8_t));
207 | }
208 |
209 | void AES::ShiftRows(uint8_t state[4][Nb]) {
210 | ShiftRow(state, 1, 1);
211 | ShiftRow(state, 2, 2);
212 | ShiftRow(state, 3, 3);
213 | }
214 |
215 | uint8_t AES::xtime(uint8_t b) // multiply on x
216 | {
217 | return (b << 1) ^ (((b >> 7) & 1) * 0x1b);
218 | }
219 |
220 | void AES::MixColumns(uint8_t state[4][Nb]) {
221 | uint8_t temp_state[4][Nb];
222 |
223 | for (auto &i: temp_state) {
224 | memset(i, 0, 4);
225 | }
226 |
227 | for (size_t i = 0; i < 4; ++i) {
228 | for (size_t k = 0; k < 4; ++k) {
229 | for (size_t j = 0; j < 4; ++j) {
230 | if (CMDS[i][k] == 1)
231 | temp_state[i][j] ^= state[k][j];
232 | else
233 | temp_state[i][j] ^= GF_MUL_TABLE[CMDS[i][k]][state[k][j]];
234 | }
235 | }
236 | }
237 |
238 | for (size_t i = 0; i < 4; ++i) {
239 | memcpy(state[i], temp_state[i], 4);
240 | }
241 | }
242 |
243 | void AES::AddRoundKey(uint8_t state[4][Nb], const uint8_t *key) {
244 | size_t i, j;
245 | for (i = 0; i < 4; i++) {
246 | for (j = 0; j < Nb; j++) {
247 | state[i][j] = state[i][j] ^ key[i + 4 * j];
248 | }
249 | }
250 | }
251 |
252 | void AES::SubWord(uint8_t *a) {
253 | int i;
254 | for (i = 0; i < 4; i++) {
255 | a[i] = sbox[a[i] / 16][a[i] % 16];
256 | }
257 | }
258 |
259 | void AES::RotWord(uint8_t *a) {
260 | uint8_t c = a[0];
261 | a[0] = a[1];
262 | a[1] = a[2];
263 | a[2] = a[3];
264 | a[3] = c;
265 | }
266 |
267 | void AES::XorWords(const uint8_t *a, const uint8_t *b, uint8_t *c) {
268 | int i;
269 | for (i = 0; i < 4; i++) {
270 | c[i] = a[i] ^ b[i];
271 | }
272 | }
273 |
274 | void AES::Rcon(uint8_t *a, const size_t n) {
275 | size_t i;
276 | uint8_t c = 1;
277 | for (i = 0; i < n - 1; i++) {
278 | c = xtime(c);
279 | }
280 |
281 | a[0] = c;
282 | a[1] = a[2] = a[3] = 0;
283 | }
284 |
285 | void AES::KeyExpansion(const uint8_t key[], uint8_t w[]) const {
286 | uint8_t temp[4];
287 | uint8_t rcon[4];
288 |
289 | size_t i = 0;
290 | while (i < 4 * Nk) {
291 | w[i] = key[i];
292 | i++;
293 | }
294 |
295 | i = 4 * Nk;
296 | while (i < 4 * Nb * (Nr + 1)) {
297 | temp[0] = w[i - 4 + 0];
298 | temp[1] = w[i - 4 + 1];
299 | temp[2] = w[i - 4 + 2];
300 | temp[3] = w[i - 4 + 3];
301 |
302 | if (i / 4 % Nk == 0) {
303 | RotWord(temp);
304 | SubWord(temp);
305 | Rcon(rcon, i / (Nk * 4));
306 | XorWords(temp, rcon, temp);
307 | } else if (Nk > 6 && i / 4 % Nk == 4) {
308 | SubWord(temp);
309 | }
310 |
311 | w[i + 0] = w[i - 4 * Nk] ^ temp[0];
312 | w[i + 1] = w[i + 1 - 4 * Nk] ^ temp[1];
313 | w[i + 2] = w[i + 2 - 4 * Nk] ^ temp[2];
314 | w[i + 3] = w[i + 3 - 4 * Nk] ^ temp[3];
315 | i += 4;
316 | }
317 | }
318 |
319 | void AES::InvSubBytes(uint8_t state[4][Nb]) {
320 | size_t i, j;
321 | uint8_t t;
322 | for (i = 0; i < 4; i++) {
323 | for (j = 0; j < Nb; j++) {
324 | t = state[i][j];
325 | state[i][j] = inv_sbox[t / 16][t % 16];
326 | }
327 | }
328 | }
329 |
330 | void AES::InvMixColumns(uint8_t state[4][Nb]) {
331 | uint8_t temp_state[4][Nb];
332 |
333 | for (auto &i: temp_state) {
334 | memset(i, 0, 4);
335 | }
336 |
337 | for (size_t i = 0; i < 4; ++i) {
338 | for (size_t k = 0; k < 4; ++k) {
339 | for (size_t j = 0; j < 4; ++j) {
340 | temp_state[i][j] ^= GF_MUL_TABLE[INV_CMDS[i][k]][state[k][j]];
341 | }
342 | }
343 | }
344 |
345 | for (size_t i = 0; i < 4; ++i) {
346 | memcpy(state[i], temp_state[i], 4);
347 | }
348 | }
349 |
350 | void AES::InvShiftRows(uint8_t state[4][Nb]) {
351 | ShiftRow(state, 1, Nb - 1);
352 | ShiftRow(state, 2, Nb - 2);
353 | ShiftRow(state, 3, Nb - 3);
354 | }
355 |
356 | void AES::XorBlocks(const uint8_t *a, const uint8_t *b,
357 | uint8_t *c, const size_t len) {
358 | for (size_t i = 0; i < len; i++) {
359 | c[i] = a[i] ^ b[i];
360 | }
361 | }
362 |
363 | void AES::printHexArray(uint8_t a[], const size_t n) {
364 | for (size_t i = 0; i < n; i++) {
365 | printf("%02x ", a[i]);
366 | }
367 | }
368 |
369 | void AES::printHexVector(const std::vector &a) {
370 | for (const auto i: a) {
371 | printf("%02x ", i);
372 | }
373 | }
374 |
375 | std::vector AES::ArrayToVector(uint8_t *a,
376 | const size_t len) {
377 | std::vector v(a, a + len * sizeof(uint8_t));
378 | return v;
379 | }
380 |
381 | uint8_t* AES::VectorToArray(std::vector &a) {
382 | return a.data();
383 | }
384 |
385 | std::vector AES::EncryptECB(std::vector in,
386 | std::vector key) {
387 | return EncryptECB(VectorToArray(in), in.size(), VectorToArray(key));
388 | }
389 |
390 | std::vector AES::DecryptECB(std::vector in,
391 | std::vector key) {
392 | return DecryptECB(VectorToArray(in), in.size(), VectorToArray(key));
393 | }
394 |
395 | std::vector AES::EncryptCBC(std::vector in,
396 | std::vector key,
397 | std::vector iv) {
398 | return EncryptCBC(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv));
399 | }
400 |
401 | std::vector AES::DecryptCBC(std::vector in,
402 | std::vector key,
403 | std::vector iv) {
404 | return DecryptCBC(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv));
405 | }
406 |
407 | std::vector AES::EncryptCFB(std::vector in,
408 | std::vector key,
409 | std::vector iv) {
410 | return EncryptCFB(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv));
411 | }
412 |
413 | std::vector AES::DecryptCFB(std::vector in,
414 | std::vector key,
415 | std::vector iv) {
416 | return DecryptCFB(VectorToArray(in), in.size(), VectorToArray(key), VectorToArray(iv));
417 | }
418 |
--------------------------------------------------------------------------------
/app/src/main/cpp/src/AES.h:
--------------------------------------------------------------------------------
1 | #ifndef _AES_H_
2 | #define _AES_H_
3 |
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | enum class AESKeyLength { AES_128, AES_192, AES_256 };
12 |
13 | class AES {
14 | private:
15 | static constexpr uint32_t Nb = 4;
16 | static constexpr uint32_t blockBytesLen = 4 * Nb * sizeof(uint8_t);
17 |
18 | uint32_t Nk;
19 | uint32_t Nr;
20 |
21 | static void SubBytes(uint8_t state[4][Nb]);
22 |
23 | static void ShiftRow(uint8_t state[4][Nb], size_t i,
24 | size_t n); // shift row i on n positions
25 |
26 | static void ShiftRows(uint8_t state[4][Nb]);
27 |
28 | static uint8_t xtime(uint8_t b); // multiply on x
29 |
30 | static void MixColumns(uint8_t state[4][Nb]);
31 |
32 | static void AddRoundKey(uint8_t state[4][Nb], const uint8_t *key);
33 |
34 | static void SubWord(uint8_t *a);
35 |
36 | static void RotWord(uint8_t *a);
37 |
38 | static void XorWords(const uint8_t *a, const uint8_t *b, uint8_t *c);
39 |
40 | static void Rcon(uint8_t *a, size_t n);
41 |
42 | static void InvSubBytes(uint8_t state[4][Nb]);
43 |
44 | static void InvMixColumns(uint8_t state[4][Nb]);
45 |
46 | static void InvShiftRows(uint8_t state[4][Nb]);
47 |
48 | static void CheckLength(size_t len);
49 |
50 | void KeyExpansion(const uint8_t key[], uint8_t w[]) const;
51 |
52 | void EncryptBlock(const uint8_t in[], uint8_t out[],
53 | uint8_t key[]) const;
54 |
55 | void DecryptBlock(const uint8_t in[], uint8_t out[],
56 | uint8_t key[]) const;
57 |
58 | static void XorBlocks(const uint8_t *a, const uint8_t *b,
59 | uint8_t *c, size_t len);
60 |
61 | static std::vector ArrayToVector(uint8_t *a, size_t len);
62 |
63 | static uint8_t* VectorToArray(std::vector &a);
64 |
65 | public:
66 | explicit AES(const AESKeyLength keyLength = AESKeyLength::AES_256);
67 |
68 | std::vector EncryptECB(const uint8_t in[], size_t inLen,
69 | const uint8_t key[]);
70 |
71 | std::vector DecryptECB(const uint8_t in[], size_t inLen,
72 | const uint8_t key[]);
73 |
74 | std::vector EncryptCBC(const uint8_t in[], size_t inLen,
75 | const uint8_t key[], const uint8_t *iv);
76 |
77 | std::vector DecryptCBC(const uint8_t in[], size_t inLen,
78 | const uint8_t key[], const uint8_t *iv);
79 |
80 | std::vector EncryptCFB(const uint8_t in[], size_t inLen,
81 | const uint8_t key[], const uint8_t *iv);
82 |
83 | std::vector DecryptCFB(const uint8_t in[], size_t inLen,
84 | const uint8_t key[], const uint8_t *iv);
85 |
86 | std::vector EncryptECB(std::vector in,
87 | std::vector key);
88 |
89 | std::vector DecryptECB(std::vector in,
90 | std::vector key);
91 |
92 | std::vector EncryptCBC(std::vector in,
93 | std::vector key,
94 | std::vector iv);
95 |
96 | std::vector DecryptCBC(std::vector in,
97 | std::vector key,
98 | std::vector iv);
99 |
100 | std::vector EncryptCFB(std::vector in,
101 | std::vector key,
102 | std::vector iv);
103 |
104 | std::vector DecryptCFB(std::vector in,
105 | std::vector key,
106 | std::vector iv);
107 |
108 | static void printHexArray(uint8_t a[], size_t n);
109 |
110 | static void printHexVector(const std::vector &a);
111 | };
112 |
113 | const uint8_t sbox[16][16] = {
114 | {
115 | 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b,
116 | 0xfe, 0xd7, 0xab, 0x76
117 | },
118 | {
119 | 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf,
120 | 0x9c, 0xa4, 0x72, 0xc0
121 | },
122 | {
123 | 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1,
124 | 0x71, 0xd8, 0x31, 0x15
125 | },
126 | {
127 | 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2,
128 | 0xeb, 0x27, 0xb2, 0x75
129 | },
130 | {
131 | 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3,
132 | 0x29, 0xe3, 0x2f, 0x84
133 | },
134 | {
135 | 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39,
136 | 0x4a, 0x4c, 0x58, 0xcf
137 | },
138 | {
139 | 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f,
140 | 0x50, 0x3c, 0x9f, 0xa8
141 | },
142 | {
143 | 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21,
144 | 0x10, 0xff, 0xf3, 0xd2
145 | },
146 | {
147 | 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d,
148 | 0x64, 0x5d, 0x19, 0x73
149 | },
150 | {
151 | 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14,
152 | 0xde, 0x5e, 0x0b, 0xdb
153 | },
154 | {
155 | 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62,
156 | 0x91, 0x95, 0xe4, 0x79
157 | },
158 | {
159 | 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea,
160 | 0x65, 0x7a, 0xae, 0x08
161 | },
162 | {
163 | 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f,
164 | 0x4b, 0xbd, 0x8b, 0x8a
165 | },
166 | {
167 | 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9,
168 | 0x86, 0xc1, 0x1d, 0x9e
169 | },
170 | {
171 | 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9,
172 | 0xce, 0x55, 0x28, 0xdf
173 | },
174 | {
175 | 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f,
176 | 0xb0, 0x54, 0xbb, 0x16
177 | }
178 | };
179 |
180 | const uint8_t inv_sbox[16][16] = {
181 | {
182 | 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e,
183 | 0x81, 0xf3, 0xd7, 0xfb
184 | },
185 | {
186 | 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44,
187 | 0xc4, 0xde, 0xe9, 0xcb
188 | },
189 | {
190 | 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b,
191 | 0x42, 0xfa, 0xc3, 0x4e
192 | },
193 | {
194 | 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49,
195 | 0x6d, 0x8b, 0xd1, 0x25
196 | },
197 | {
198 | 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc,
199 | 0x5d, 0x65, 0xb6, 0x92
200 | },
201 | {
202 | 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57,
203 | 0xa7, 0x8d, 0x9d, 0x84
204 | },
205 | {
206 | 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05,
207 | 0xb8, 0xb3, 0x45, 0x06
208 | },
209 | {
210 | 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03,
211 | 0x01, 0x13, 0x8a, 0x6b
212 | },
213 | {
214 | 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce,
215 | 0xf0, 0xb4, 0xe6, 0x73
216 | },
217 | {
218 | 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8,
219 | 0x1c, 0x75, 0xdf, 0x6e
220 | },
221 | {
222 | 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e,
223 | 0xaa, 0x18, 0xbe, 0x1b
224 | },
225 | {
226 | 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe,
227 | 0x78, 0xcd, 0x5a, 0xf4
228 | },
229 | {
230 | 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59,
231 | 0x27, 0x80, 0xec, 0x5f
232 | },
233 | {
234 | 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f,
235 | 0x93, 0xc9, 0x9c, 0xef
236 | },
237 | {
238 | 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c,
239 | 0x83, 0x53, 0x99, 0x61
240 | },
241 | {
242 | 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63,
243 | 0x55, 0x21, 0x0c, 0x7d
244 | }
245 | };
246 |
247 | /// Galois Multiplication lookup tables
248 | static const uint8_t GF_MUL_TABLE[15][256] = {
249 | {},
250 | {},
251 |
252 | // mul 2
253 | {
254 | 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16,
255 | 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e,
256 | 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46,
257 | 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e,
258 | 0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76,
259 | 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e,
260 | 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6,
261 | 0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe,
262 | 0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6,
263 | 0xd8, 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee,
264 | 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe, 0x1b, 0x19, 0x1f, 0x1d,
265 | 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09, 0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05,
266 | 0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d,
267 | 0x23, 0x21, 0x27, 0x25, 0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55,
268 | 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41, 0x47, 0x45, 0x7b, 0x79, 0x7f, 0x7d,
269 | 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65,
270 | 0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d,
271 | 0x83, 0x81, 0x87, 0x85, 0xbb, 0xb9, 0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5,
272 | 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5, 0xdb, 0xd9, 0xdf, 0xdd,
273 | 0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5,
274 | 0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1, 0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed,
275 | 0xe3, 0xe1, 0xe7, 0xe5
276 | },
277 |
278 | // mul 3
279 | {
280 | 0x00, 0x03, 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d,
281 | 0x14, 0x17, 0x12, 0x11, 0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f, 0x3a, 0x39,
282 | 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21, 0x60, 0x63, 0x66, 0x65,
283 | 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b, 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71,
284 | 0x50, 0x53, 0x56, 0x55, 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d,
285 | 0x44, 0x47, 0x42, 0x41, 0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9,
286 | 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1, 0xf0, 0xf3, 0xf6, 0xf5,
287 | 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed, 0xe4, 0xe7, 0xe2, 0xe1,
288 | 0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf, 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd,
289 | 0xb4, 0xb7, 0xb2, 0xb1, 0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99,
290 | 0x88, 0x8b, 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81, 0x9b, 0x98, 0x9d, 0x9e,
291 | 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86, 0x8f, 0x8c, 0x89, 0x8a,
292 | 0xab, 0xa8, 0xad, 0xae, 0xa7, 0xa4, 0xa1, 0xa2, 0xb3, 0xb0, 0xb5, 0xb6,
293 | 0xbf, 0xbc, 0xb9, 0xba, 0xfb, 0xf8, 0xfd, 0xfe, 0xf7, 0xf4, 0xf1, 0xf2,
294 | 0xe3, 0xe0, 0xe5, 0xe6, 0xef, 0xec, 0xe9, 0xea, 0xcb, 0xc8, 0xcd, 0xce,
295 | 0xc7, 0xc4, 0xc1, 0xc2, 0xd3, 0xd0, 0xd5, 0xd6, 0xdf, 0xdc, 0xd9, 0xda,
296 | 0x5b, 0x58, 0x5d, 0x5e, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46,
297 | 0x4f, 0x4c, 0x49, 0x4a, 0x6b, 0x68, 0x6d, 0x6e, 0x67, 0x64, 0x61, 0x62,
298 | 0x73, 0x70, 0x75, 0x76, 0x7f, 0x7c, 0x79, 0x7a, 0x3b, 0x38, 0x3d, 0x3e,
299 | 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26, 0x2f, 0x2c, 0x29, 0x2a,
300 | 0x0b, 0x08, 0x0d, 0x0e, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16,
301 | 0x1f, 0x1c, 0x19, 0x1a
302 | },
303 |
304 | {},
305 | {},
306 | {},
307 | {},
308 | {},
309 |
310 | // mul 9
311 | {
312 | 0x00, 0x09, 0x12, 0x1b, 0x24, 0x2d, 0x36, 0x3f, 0x48, 0x41, 0x5a, 0x53,
313 | 0x6c, 0x65, 0x7e, 0x77, 0x90, 0x99, 0x82, 0x8b, 0xb4, 0xbd, 0xa6, 0xaf,
314 | 0xd8, 0xd1, 0xca, 0xc3, 0xfc, 0xf5, 0xee, 0xe7, 0x3b, 0x32, 0x29, 0x20,
315 | 0x1f, 0x16, 0x0d, 0x04, 0x73, 0x7a, 0x61, 0x68, 0x57, 0x5e, 0x45, 0x4c,
316 | 0xab, 0xa2, 0xb9, 0xb0, 0x8f, 0x86, 0x9d, 0x94, 0xe3, 0xea, 0xf1, 0xf8,
317 | 0xc7, 0xce, 0xd5, 0xdc, 0x76, 0x7f, 0x64, 0x6d, 0x52, 0x5b, 0x40, 0x49,
318 | 0x3e, 0x37, 0x2c, 0x25, 0x1a, 0x13, 0x08, 0x01, 0xe6, 0xef, 0xf4, 0xfd,
319 | 0xc2, 0xcb, 0xd0, 0xd9, 0xae, 0xa7, 0xbc, 0xb5, 0x8a, 0x83, 0x98, 0x91,
320 | 0x4d, 0x44, 0x5f, 0x56, 0x69, 0x60, 0x7b, 0x72, 0x05, 0x0c, 0x17, 0x1e,
321 | 0x21, 0x28, 0x33, 0x3a, 0xdd, 0xd4, 0xcf, 0xc6, 0xf9, 0xf0, 0xeb, 0xe2,
322 | 0x95, 0x9c, 0x87, 0x8e, 0xb1, 0xb8, 0xa3, 0xaa, 0xec, 0xe5, 0xfe, 0xf7,
323 | 0xc8, 0xc1, 0xda, 0xd3, 0xa4, 0xad, 0xb6, 0xbf, 0x80, 0x89, 0x92, 0x9b,
324 | 0x7c, 0x75, 0x6e, 0x67, 0x58, 0x51, 0x4a, 0x43, 0x34, 0x3d, 0x26, 0x2f,
325 | 0x10, 0x19, 0x02, 0x0b, 0xd7, 0xde, 0xc5, 0xcc, 0xf3, 0xfa, 0xe1, 0xe8,
326 | 0x9f, 0x96, 0x8d, 0x84, 0xbb, 0xb2, 0xa9, 0xa0, 0x47, 0x4e, 0x55, 0x5c,
327 | 0x63, 0x6a, 0x71, 0x78, 0x0f, 0x06, 0x1d, 0x14, 0x2b, 0x22, 0x39, 0x30,
328 | 0x9a, 0x93, 0x88, 0x81, 0xbe, 0xb7, 0xac, 0xa5, 0xd2, 0xdb, 0xc0, 0xc9,
329 | 0xf6, 0xff, 0xe4, 0xed, 0x0a, 0x03, 0x18, 0x11, 0x2e, 0x27, 0x3c, 0x35,
330 | 0x42, 0x4b, 0x50, 0x59, 0x66, 0x6f, 0x74, 0x7d, 0xa1, 0xa8, 0xb3, 0xba,
331 | 0x85, 0x8c, 0x97, 0x9e, 0xe9, 0xe0, 0xfb, 0xf2, 0xcd, 0xc4, 0xdf, 0xd6,
332 | 0x31, 0x38, 0x23, 0x2a, 0x15, 0x1c, 0x07, 0x0e, 0x79, 0x70, 0x6b, 0x62,
333 | 0x5d, 0x54, 0x4f, 0x46
334 | },
335 |
336 | {},
337 |
338 | // mul 11
339 | {
340 | 0x00, 0x0b, 0x16, 0x1d, 0x2c, 0x27, 0x3a, 0x31, 0x58, 0x53, 0x4e, 0x45,
341 | 0x74, 0x7f, 0x62, 0x69, 0xb0, 0xbb, 0xa6, 0xad, 0x9c, 0x97, 0x8a, 0x81,
342 | 0xe8, 0xe3, 0xfe, 0xf5, 0xc4, 0xcf, 0xd2, 0xd9, 0x7b, 0x70, 0x6d, 0x66,
343 | 0x57, 0x5c, 0x41, 0x4a, 0x23, 0x28, 0x35, 0x3e, 0x0f, 0x04, 0x19, 0x12,
344 | 0xcb, 0xc0, 0xdd, 0xd6, 0xe7, 0xec, 0xf1, 0xfa, 0x93, 0x98, 0x85, 0x8e,
345 | 0xbf, 0xb4, 0xa9, 0xa2, 0xf6, 0xfd, 0xe0, 0xeb, 0xda, 0xd1, 0xcc, 0xc7,
346 | 0xae, 0xa5, 0xb8, 0xb3, 0x82, 0x89, 0x94, 0x9f, 0x46, 0x4d, 0x50, 0x5b,
347 | 0x6a, 0x61, 0x7c, 0x77, 0x1e, 0x15, 0x08, 0x03, 0x32, 0x39, 0x24, 0x2f,
348 | 0x8d, 0x86, 0x9b, 0x90, 0xa1, 0xaa, 0xb7, 0xbc, 0xd5, 0xde, 0xc3, 0xc8,
349 | 0xf9, 0xf2, 0xef, 0xe4, 0x3d, 0x36, 0x2b, 0x20, 0x11, 0x1a, 0x07, 0x0c,
350 | 0x65, 0x6e, 0x73, 0x78, 0x49, 0x42, 0x5f, 0x54, 0xf7, 0xfc, 0xe1, 0xea,
351 | 0xdb, 0xd0, 0xcd, 0xc6, 0xaf, 0xa4, 0xb9, 0xb2, 0x83, 0x88, 0x95, 0x9e,
352 | 0x47, 0x4c, 0x51, 0x5a, 0x6b, 0x60, 0x7d, 0x76, 0x1f, 0x14, 0x09, 0x02,
353 | 0x33, 0x38, 0x25, 0x2e, 0x8c, 0x87, 0x9a, 0x91, 0xa0, 0xab, 0xb6, 0xbd,
354 | 0xd4, 0xdf, 0xc2, 0xc9, 0xf8, 0xf3, 0xee, 0xe5, 0x3c, 0x37, 0x2a, 0x21,
355 | 0x10, 0x1b, 0x06, 0x0d, 0x64, 0x6f, 0x72, 0x79, 0x48, 0x43, 0x5e, 0x55,
356 | 0x01, 0x0a, 0x17, 0x1c, 0x2d, 0x26, 0x3b, 0x30, 0x59, 0x52, 0x4f, 0x44,
357 | 0x75, 0x7e, 0x63, 0x68, 0xb1, 0xba, 0xa7, 0xac, 0x9d, 0x96, 0x8b, 0x80,
358 | 0xe9, 0xe2, 0xff, 0xf4, 0xc5, 0xce, 0xd3, 0xd8, 0x7a, 0x71, 0x6c, 0x67,
359 | 0x56, 0x5d, 0x40, 0x4b, 0x22, 0x29, 0x34, 0x3f, 0x0e, 0x05, 0x18, 0x13,
360 | 0xca, 0xc1, 0xdc, 0xd7, 0xe6, 0xed, 0xf0, 0xfb, 0x92, 0x99, 0x84, 0x8f,
361 | 0xbe, 0xb5, 0xa8, 0xa3
362 | },
363 |
364 | {},
365 |
366 | // mul 13
367 | {
368 | 0x00, 0x0d, 0x1a, 0x17, 0x34, 0x39, 0x2e, 0x23, 0x68, 0x65, 0x72, 0x7f,
369 | 0x5c, 0x51, 0x46, 0x4b, 0xd0, 0xdd, 0xca, 0xc7, 0xe4, 0xe9, 0xfe, 0xf3,
370 | 0xb8, 0xb5, 0xa2, 0xaf, 0x8c, 0x81, 0x96, 0x9b, 0xbb, 0xb6, 0xa1, 0xac,
371 | 0x8f, 0x82, 0x95, 0x98, 0xd3, 0xde, 0xc9, 0xc4, 0xe7, 0xea, 0xfd, 0xf0,
372 | 0x6b, 0x66, 0x71, 0x7c, 0x5f, 0x52, 0x45, 0x48, 0x03, 0x0e, 0x19, 0x14,
373 | 0x37, 0x3a, 0x2d, 0x20, 0x6d, 0x60, 0x77, 0x7a, 0x59, 0x54, 0x43, 0x4e,
374 | 0x05, 0x08, 0x1f, 0x12, 0x31, 0x3c, 0x2b, 0x26, 0xbd, 0xb0, 0xa7, 0xaa,
375 | 0x89, 0x84, 0x93, 0x9e, 0xd5, 0xd8, 0xcf, 0xc2, 0xe1, 0xec, 0xfb, 0xf6,
376 | 0xd6, 0xdb, 0xcc, 0xc1, 0xe2, 0xef, 0xf8, 0xf5, 0xbe, 0xb3, 0xa4, 0xa9,
377 | 0x8a, 0x87, 0x90, 0x9d, 0x06, 0x0b, 0x1c, 0x11, 0x32, 0x3f, 0x28, 0x25,
378 | 0x6e, 0x63, 0x74, 0x79, 0x5a, 0x57, 0x40, 0x4d, 0xda, 0xd7, 0xc0, 0xcd,
379 | 0xee, 0xe3, 0xf4, 0xf9, 0xb2, 0xbf, 0xa8, 0xa5, 0x86, 0x8b, 0x9c, 0x91,
380 | 0x0a, 0x07, 0x10, 0x1d, 0x3e, 0x33, 0x24, 0x29, 0x62, 0x6f, 0x78, 0x75,
381 | 0x56, 0x5b, 0x4c, 0x41, 0x61, 0x6c, 0x7b, 0x76, 0x55, 0x58, 0x4f, 0x42,
382 | 0x09, 0x04, 0x13, 0x1e, 0x3d, 0x30, 0x27, 0x2a, 0xb1, 0xbc, 0xab, 0xa6,
383 | 0x85, 0x88, 0x9f, 0x92, 0xd9, 0xd4, 0xc3, 0xce, 0xed, 0xe0, 0xf7, 0xfa,
384 | 0xb7, 0xba, 0xad, 0xa0, 0x83, 0x8e, 0x99, 0x94, 0xdf, 0xd2, 0xc5, 0xc8,
385 | 0xeb, 0xe6, 0xf1, 0xfc, 0x67, 0x6a, 0x7d, 0x70, 0x53, 0x5e, 0x49, 0x44,
386 | 0x0f, 0x02, 0x15, 0x18, 0x3b, 0x36, 0x21, 0x2c, 0x0c, 0x01, 0x16, 0x1b,
387 | 0x38, 0x35, 0x22, 0x2f, 0x64, 0x69, 0x7e, 0x73, 0x50, 0x5d, 0x4a, 0x47,
388 | 0xdc, 0xd1, 0xc6, 0xcb, 0xe8, 0xe5, 0xf2, 0xff, 0xb4, 0xb9, 0xae, 0xa3,
389 | 0x80, 0x8d, 0x9a, 0x97
390 | },
391 |
392 | // mul 14
393 | {
394 | 0x00, 0x0e, 0x1c, 0x12, 0x38, 0x36, 0x24, 0x2a, 0x70, 0x7e, 0x6c, 0x62,
395 | 0x48, 0x46, 0x54, 0x5a, 0xe0, 0xee, 0xfc, 0xf2, 0xd8, 0xd6, 0xc4, 0xca,
396 | 0x90, 0x9e, 0x8c, 0x82, 0xa8, 0xa6, 0xb4, 0xba, 0xdb, 0xd5, 0xc7, 0xc9,
397 | 0xe3, 0xed, 0xff, 0xf1, 0xab, 0xa5, 0xb7, 0xb9, 0x93, 0x9d, 0x8f, 0x81,
398 | 0x3b, 0x35, 0x27, 0x29, 0x03, 0x0d, 0x1f, 0x11, 0x4b, 0x45, 0x57, 0x59,
399 | 0x73, 0x7d, 0x6f, 0x61, 0xad, 0xa3, 0xb1, 0xbf, 0x95, 0x9b, 0x89, 0x87,
400 | 0xdd, 0xd3, 0xc1, 0xcf, 0xe5, 0xeb, 0xf9, 0xf7, 0x4d, 0x43, 0x51, 0x5f,
401 | 0x75, 0x7b, 0x69, 0x67, 0x3d, 0x33, 0x21, 0x2f, 0x05, 0x0b, 0x19, 0x17,
402 | 0x76, 0x78, 0x6a, 0x64, 0x4e, 0x40, 0x52, 0x5c, 0x06, 0x08, 0x1a, 0x14,
403 | 0x3e, 0x30, 0x22, 0x2c, 0x96, 0x98, 0x8a, 0x84, 0xae, 0xa0, 0xb2, 0xbc,
404 | 0xe6, 0xe8, 0xfa, 0xf4, 0xde, 0xd0, 0xc2, 0xcc, 0x41, 0x4f, 0x5d, 0x53,
405 | 0x79, 0x77, 0x65, 0x6b, 0x31, 0x3f, 0x2d, 0x23, 0x09, 0x07, 0x15, 0x1b,
406 | 0xa1, 0xaf, 0xbd, 0xb3, 0x99, 0x97, 0x85, 0x8b, 0xd1, 0xdf, 0xcd, 0xc3,
407 | 0xe9, 0xe7, 0xf5, 0xfb, 0x9a, 0x94, 0x86, 0x88, 0xa2, 0xac, 0xbe, 0xb0,
408 | 0xea, 0xe4, 0xf6, 0xf8, 0xd2, 0xdc, 0xce, 0xc0, 0x7a, 0x74, 0x66, 0x68,
409 | 0x42, 0x4c, 0x5e, 0x50, 0x0a, 0x04, 0x16, 0x18, 0x32, 0x3c, 0x2e, 0x20,
410 | 0xec, 0xe2, 0xf0, 0xfe, 0xd4, 0xda, 0xc8, 0xc6, 0x9c, 0x92, 0x80, 0x8e,
411 | 0xa4, 0xaa, 0xb8, 0xb6, 0x0c, 0x02, 0x10, 0x1e, 0x34, 0x3a, 0x28, 0x26,
412 | 0x7c, 0x72, 0x60, 0x6e, 0x44, 0x4a, 0x58, 0x56, 0x37, 0x39, 0x2b, 0x25,
413 | 0x0f, 0x01, 0x13, 0x1d, 0x47, 0x49, 0x5b, 0x55, 0x7f, 0x71, 0x63, 0x6d,
414 | 0xd7, 0xd9, 0xcb, 0xc5, 0xef, 0xe1, 0xf3, 0xfd, 0xa7, 0xa9, 0xbb, 0xb5,
415 | 0x9f, 0x91, 0x83, 0x8d
416 | }
417 | };
418 |
419 | /// circulant MDS matrix
420 | static const uint8_t CMDS[4][4] = {
421 | {2, 3, 1, 1}, {1, 2, 3, 1}, {1, 1, 2, 3}, {3, 1, 1, 2}
422 | };
423 |
424 | /// Inverse circulant MDS matrix
425 | static const uint8_t INV_CMDS[4][4] = {
426 | {14, 11, 13, 9}, {9, 14, 11, 13}, {13, 9, 14, 11}, {11, 13, 9, 14}
427 | };
428 |
429 | #endif
430 |
--------------------------------------------------------------------------------
/app/src/main/cpp/src/ncm.cpp:
--------------------------------------------------------------------------------
1 | #include "AES.h"
2 | #include "ncm.h"
3 | #include "json.hpp"
4 | #include
5 | #include
6 |
7 | #ifndef READ_DATA_BUFFER_SIZE
8 | #define READ_DATA_BUFFER_SIZE 8192
9 | #endif
10 |
11 | constexpr uint8_t MAGIC_HEADER[] = {0X43, 0X54, 0X45, 0X4E, 0X46, 0X44, 0X41, 0X4D};
12 | constexpr uint8_t CORE_KEY[] = {
13 | 0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78,
14 | 0x57
15 | };
16 | constexpr uint8_t META_KEY[] = {
17 | 0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27,
18 | 0x28
19 | };
20 |
21 | std::string base64_decode(const std::string_view in) {
22 | const static uint8_t TABLE[] = {
23 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
24 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
25 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
26 | 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0,
27 | 63, 52, 53, 54, 55, 56, 57, 58,
28 | 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0,
29 | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
30 | 13, 14, 15, 16, 17, 18, 19, 20, 21,
31 | 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26,
32 | 27, 28, 29, 30, 31, 32, 33, 34, 35,
33 | 36, 37, 38, 39, 40, 41, 42, 43, 44,
34 | 45, 46, 47, 48, 49, 50, 51
35 | };
36 |
37 | std::string out;
38 | out.reserve(in.size() * 3 / 4);
39 |
40 | uint32_t buf{};
41 | int n{};
42 | for (const auto c: in) {
43 | if (c == '=') {
44 | break;
45 | }
46 | buf = buf << 6 | TABLE[c];
47 | n += 6;
48 | if (n >= 8) {
49 | n -= 8;
50 | out.push_back(static_cast(buf >> n & 0xFF));
51 | }
52 | }
53 | return out;
54 | }
55 |
56 | NcmDumpError ncm_dump(std::ifstream &input, std::filesystem::path &outputFolder) {
57 | if (!input) {
58 | return NcmDumpError::InvalidInputStream;
59 | }
60 | if (!exists(outputFolder) && !create_directories(outputFolder)) {
61 | return NcmDumpError::InvalidOutputFolder;
62 | }
63 |
64 | char header[8]{0};
65 | input.read(header, 8);
66 | if (memcmp(header, MAGIC_HEADER, 8) != 0) {
67 | return NcmDumpError::InvalidNcmFileHeader;
68 | }
69 |
70 | input.seekg(2, std::ios::cur); // skip 2 bytes
71 |
72 | uint8_t lenBuf[4]{0};
73 | input.read(reinterpret_cast(lenBuf), 4);
74 | uint32_t len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]);
75 |
76 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer
77 |
78 | std::vector _key(len);
79 | input.read(reinterpret_cast(_key.data()), len);
80 | for (size_t i{}; i < len; ++i) {
81 | _key[i] ^= 0x64;
82 | }
83 |
84 | // AES-ECB Pkcs7padding
85 | AES aes(AESKeyLength::AES_128);
86 |
87 | auto keyBox(aes.DecryptECB(_key.data(), _key.size(), CORE_KEY));
88 | keyBox.erase(keyBox.begin(), keyBox.begin() + 17); // substring `neteasecloudmusic`
89 | keyBox.resize(256);
90 | std::fill(std::find(keyBox.begin(), keyBox.end(), '\r'), keyBox.end(), 0); // fill 0
91 | { // RC4 init
92 | size_t keylen = strlen(reinterpret_cast(keyBox.data()));
93 | uint8_t ch[256]{0};
94 | uint8_t j = 0;
95 | for (size_t i{}; i < 256; ++i) {
96 | ch[i] = static_cast(i);
97 | }
98 | for (size_t i{}; i < 256; ++i) {
99 | j = j + ch[i] + keyBox[i % keylen] & 0xFF;
100 | std::swap(ch[i], ch[j]);
101 | }
102 | keyBox = std::vector(ch, ch + 256);
103 | }
104 |
105 | // read music info
106 | input.read(reinterpret_cast(lenBuf), 4);
107 | len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]);
108 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer
109 |
110 | std::vector metadata(len);
111 | input.read(reinterpret_cast(metadata.data()), len);
112 | if (!input) {
113 | return NcmDumpError::CannotReadMusicInfo;
114 | }
115 |
116 | for (size_t i{}; i < len; ++i) {
117 | metadata[i] ^= 0x63;
118 | }
119 | std::string meta(metadata.begin(), metadata.end());
120 |
121 | // decrypt music info
122 | meta = meta.substr(22); // substring `163 key(Don't modify):`
123 | const std::string decBase64 = base64_decode(meta);
124 | const auto decMeta(aes.DecryptECB(reinterpret_cast(decBase64.c_str()),
125 | decBase64.length(),
126 | META_KEY));
127 |
128 | std::string metaJsonString(decMeta.begin(), decMeta.end());
129 | metaJsonString = metaJsonString.substr(6); // substring `music:`
130 | metaJsonString.erase(metaJsonString.find_last_of('}') + 1); // fix '\a' at the end of string
131 | auto metaJson = nlohmann::json::parse(metaJsonString);
132 | std::string fileFormat = metaJson["format"];
133 | std::transform(fileFormat.begin(), fileFormat.end(), fileFormat.begin(),
134 | [](const char c) { return std::tolower(c); });
135 | // make file format to lower case
136 | if (fileFormat.empty()) {
137 | return NcmDumpError::UnknownFileFormat;
138 | }
139 |
140 | // read music cover data
141 | input.seekg(9, std::ios::cur); // seek crc & gap
142 | if (!input) {
143 | return NcmDumpError::CannotReadMusicCover;
144 | }
145 |
146 | input.read(reinterpret_cast(lenBuf), 4);
147 | len = (lenBuf[3] << 8 | lenBuf[2]) << 16 | (lenBuf[1] << 8 | lenBuf[0]);
148 | memset(lenBuf, 0, sizeof(lenBuf)); // reset buffer
149 | // std::vector coverData(len);
150 | // input.read(reinterpret_cast(coverData.data()), len);
151 | input.seekg(len, std::ios::cur); // skip cover data
152 |
153 | std::string musicName = metaJson["musicName"];
154 | std::string album = metaJson["album"];
155 | std::vector> artist = metaJson["artist"]; // [["Name", uid], ...]
156 | // uint32_t bitrate = metaJson["bitrate"];
157 | // uint64_t duration = metaJson["duration"];
158 |
159 | std::string artists;
160 | for (auto &v: artist) {
161 | for (auto &jobj: v) {
162 | if (jobj.is_string()) {
163 | artists.append(jobj.get());
164 | artists.append(" ");
165 | }
166 | }
167 | }
168 | // trim end
169 | if (artists.ends_with(' '))
170 | artists.erase(artists.end() - 1);
171 |
172 | // combine file name
173 | std::string fileName = artists + " - " + musicName + "." + fileFormat;
174 | std::ofstream output(outputFolder / fileName, std::ios::binary | std::ios::ate);
175 |
176 | // check stream valid
177 | if (!output) {
178 | return NcmDumpError::CannotSaveOutputFile;
179 | }
180 | if (!input) {
181 | return NcmDumpError::CannotReadMusicData;
182 | }
183 |
184 | // process music data
185 | auto buf = std::make_unique(READ_DATA_BUFFER_SIZE);
186 | // decode data
187 | while (!input.eof()) {
188 | input.read(reinterpret_cast(buf.get()), READ_DATA_BUFFER_SIZE);
189 | std::streamsize cnt = input.gcount();
190 |
191 | for (size_t i{}; i < cnt; ++i) {
192 | uint8_t j = (i + 1) & 0xff;
193 | buf[i] ^= keyBox[(keyBox[j] + keyBox[(keyBox[j] + j) & 0xff]) & 0xff];
194 | }
195 |
196 | output.write(reinterpret_cast(buf.get()), cnt);
197 | if (!output) {
198 | return NcmDumpError::CannotSaveOutputFile;
199 | }
200 | }
201 |
202 | output.flush();
203 | output.close();
204 |
205 | return NcmDumpError::Success;
206 | }
207 |
--------------------------------------------------------------------------------
/app/src/main/cpp/src/ncm.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | enum class NcmDumpError : uint8_t {
6 | Success = 0,
7 | InvalidInputStream,
8 | InvalidOutputFolder,
9 | InvalidNcmFileHeader,
10 | UnknownFileFormat,
11 | CannotReadMusicInfo,
12 | CannotReadMusicCover,
13 | CannotReadMusicData,
14 | CannotSaveOutputFile,
15 | };
16 |
17 | [[nodiscard]] NcmDumpError ncm_dump(std::ifstream &input, std::filesystem::path &outputFolder);
18 |
--------------------------------------------------------------------------------
/app/src/main/cpp/src/ncmdumper.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "ncm.h"
5 |
6 | extern "C" {
7 | JNIEXPORT jint JNICALL
8 | Java_me_kyuubiran_ncmdumper_ui_utils_Dumper_dumpNcmFile(JNIEnv *env, jclass clazz,
9 | jstring in_file_path,
10 | jstring out_file_path) {
11 |
12 | std::string inPath;
13 | std::string outPath;
14 |
15 | jboolean isCopy = false;
16 |
17 | inPath = env->GetStringUTFChars(in_file_path, &isCopy);
18 | outPath = env->GetStringUTFChars(out_file_path, &isCopy);
19 |
20 | std::ifstream ifs(inPath, std::ios::in | std::ios::binary);
21 | std::filesystem::path out(outPath);
22 | NcmDumpError err = ncm_dump(ifs, out);
23 |
24 | return static_cast(err);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.content.SharedPreferences
6 | import android.os.Build
7 | import android.os.Bundle
8 | import android.os.Environment
9 | import android.provider.Settings
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.setContent
12 | import androidx.activity.enableEdgeToEdge
13 | import androidx.compose.foundation.layout.Box
14 | import androidx.compose.foundation.layout.safeDrawingPadding
15 | import androidx.compose.foundation.layout.statusBarsPadding
16 | import androidx.compose.material.AlertDialog
17 | import androidx.compose.material.Text
18 | import androidx.compose.material.TextButton
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.DisposableEffect
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.platform.LocalLifecycleOwner
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.lifecycle.DefaultLifecycleObserver
29 | import androidx.lifecycle.Lifecycle
30 | import androidx.lifecycle.LifecycleEventObserver
31 | import androidx.lifecycle.LifecycleOwner
32 | import androidx.navigation.compose.NavHost
33 | import androidx.navigation.compose.composable
34 | import androidx.navigation.compose.rememberNavController
35 | import me.kyuubiran.ncmdumper.ui.pages.MainPage
36 | import me.kyuubiran.ncmdumper.ui.pages.SettingsPage
37 | import me.kyuubiran.ncmdumper.ui.theme.MyTheme
38 |
39 | class MainActivity : ComponentActivity() {
40 |
41 | companion object {
42 | lateinit var sp: SharedPreferences
43 | private set
44 | }
45 |
46 | override fun onCreate(savedInstanceState: Bundle?) {
47 | super.onCreate(savedInstanceState)
48 | sp = getSharedPreferences("settings", Activity.MODE_PRIVATE)
49 |
50 | enableEdgeToEdge()
51 | setContent {
52 | val navController = rememberNavController()
53 | MyTheme {
54 | RequireAllFileAccessPermissionDialog(this)
55 | Box(modifier = Modifier.safeDrawingPadding()) {
56 | NavHost(navController = navController, startDestination = "main_page") {
57 | composable("main_page") { MainPage.View(navController, this@MainActivity) }
58 | composable("settings_page") { SettingsPage.View(navController, this@MainActivity) }
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | @Composable
67 | private fun RequireAllFileAccessPermissionDialog(activity: Activity) {
68 | if (Build.VERSION.SDK_INT < 30)
69 | return
70 |
71 | var showed by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
72 |
73 | val lifecycleOwner = LocalLifecycleOwner.current
74 | DisposableEffect(lifecycleOwner) {
75 | val lifecycle = LifecycleEventObserver { _, e ->
76 | if (e != Lifecycle.Event.ON_RESUME)
77 | return@LifecycleEventObserver
78 |
79 | if (Build.VERSION.SDK_INT < 30)
80 | return@LifecycleEventObserver
81 |
82 | if (showed) {
83 | showed = !Environment.isExternalStorageManager()
84 | }
85 | }
86 |
87 | lifecycleOwner.lifecycle.addObserver(lifecycle)
88 | onDispose { lifecycleOwner.lifecycle.removeObserver(lifecycle) }
89 | }
90 |
91 | if (showed) {
92 | AlertDialog(
93 | onDismissRequest = { },
94 | confirmButton = {
95 | TextButton(
96 | onClick = {
97 | val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
98 | activity.startActivity(intent)
99 | },
100 | ) { Text(text = stringResource(id = R.string.confirm)) }
101 | },
102 | title = { Text(text = stringResource(id = R.string.title_dialog_require_all_file_access_permission)) },
103 | text = { Text(text = stringResource(id = R.string.message_dialog_require_all_file_access_permission)) },
104 | )
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper
2 |
3 | import android.app.Application
4 |
5 | class MyApplication : Application() {
6 | override fun onCreate() {
7 | super.onCreate()
8 | System.loadLibrary("ncmdumper")
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/pages/MainPage.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.pages
2 |
3 | import android.os.Environment
4 | import android.util.Log
5 | import androidx.activity.compose.BackHandler
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.isSystemInDarkTheme
10 | import androidx.compose.foundation.layout.Box
11 | import androidx.compose.foundation.layout.Column
12 | import androidx.compose.foundation.layout.Row
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.size
18 | import androidx.compose.foundation.layout.statusBarsPadding
19 | import androidx.compose.foundation.lazy.LazyColumn
20 | import androidx.compose.foundation.lazy.items
21 | import androidx.compose.foundation.shape.CircleShape
22 | import androidx.compose.foundation.text.selection.LocalTextSelectionColors
23 | import androidx.compose.foundation.text.selection.SelectionContainer
24 | import androidx.compose.foundation.text.selection.TextSelectionColors
25 | import androidx.compose.material.DropdownMenu
26 | import androidx.compose.material.DropdownMenuItem
27 | import androidx.compose.material.ExperimentalMaterialApi
28 | import androidx.compose.material.OutlinedTextField
29 | import androidx.compose.material.Text
30 | import androidx.compose.material.TextField
31 | import androidx.compose.material.TextFieldColors
32 | import androidx.compose.material.TextFieldDefaults
33 | import androidx.compose.material.TopAppBar
34 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
35 | import androidx.compose.material.pullrefresh.pullRefresh
36 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
37 | import androidx.compose.runtime.Composable
38 | import androidx.compose.runtime.CompositionLocalProvider
39 | import androidx.compose.runtime.LaunchedEffect
40 | import androidx.compose.runtime.getValue
41 | import androidx.compose.runtime.mutableStateListOf
42 | import androidx.compose.runtime.mutableStateOf
43 | import androidx.compose.runtime.remember
44 | import androidx.compose.runtime.rememberCoroutineScope
45 | import androidx.compose.runtime.setValue
46 | import androidx.compose.ui.Alignment
47 | import androidx.compose.ui.Modifier
48 | import androidx.compose.ui.draw.clip
49 | import androidx.compose.ui.graphics.Color
50 | import androidx.compose.ui.graphics.ColorFilter
51 | import androidx.compose.ui.res.painterResource
52 | import androidx.compose.ui.res.stringResource
53 | import androidx.compose.ui.unit.dp
54 | import androidx.navigation.NavHostController
55 | import kotlinx.coroutines.Dispatchers
56 | import kotlinx.coroutines.delay
57 | import kotlinx.coroutines.launch
58 | import kotlinx.coroutines.withContext
59 | import me.kyuubiran.ncmdumper.MainActivity
60 | import me.kyuubiran.ncmdumper.R
61 | import me.kyuubiran.ncmdumper.ui.theme.BackGroundColor
62 | import me.kyuubiran.ncmdumper.ui.theme.LightBlue
63 | import me.kyuubiran.ncmdumper.ui.theme.LightDark
64 | import me.kyuubiran.ncmdumper.ui.utils.Dumper
65 | import me.kyuubiran.ncmdumper.ui.views.NcmFileInfo
66 | import me.kyuubiran.ncmdumper.ui.views.NcmFileItem
67 | import java.io.File
68 |
69 | object MainPage {
70 | private val list = mutableStateListOf()
71 | private val listFiltered = mutableStateListOf()
72 |
73 | private fun reloadFiles() {
74 | val f = File(Dumper.DEFAULT_NETEASE_MUSIC_PATH)
75 | list.clear()
76 | if (!f.exists() || !f.isDirectory)
77 | return
78 |
79 | list += f.listFiles { ff -> ff.extension.lowercase() == "ncm" }?.toList() ?: emptyList()
80 |
81 | Log.i("MainFragment", "Found ${list.size} ncm files")
82 | }
83 |
84 | private fun reloadFilesFullDisk() {
85 | val f = Environment.getExternalStorageDirectory()
86 |
87 | list.clear()
88 |
89 | fun doSearch(f: File) {
90 | if (!f.exists())
91 | return
92 |
93 | if (f.isDirectory) {
94 | f.listFiles()?.forEach {
95 | doSearch(it)
96 | }
97 | } else {
98 | if (f.extension.lowercase() == "ncm")
99 | list += f
100 | }
101 | }
102 |
103 | doSearch(f)
104 | }
105 |
106 | private fun updateSearch(str: String) {
107 | if (!showSearch.value)
108 | return
109 |
110 | listFiltered.clear()
111 | if (str.isBlank()) {
112 | listFiltered += list
113 | return
114 | }
115 |
116 | val args = str.split(' ')
117 | list.forEach {
118 | listFiltered.apply {
119 | val name = it.name
120 | if (args.all { arg -> name.contains(arg, ignoreCase = true) })
121 | add(it)
122 | }
123 | }
124 | }
125 |
126 | @Composable
127 | private fun AppBar(controller: NavHostController) {
128 | var moreShowed by remember { mutableStateOf(false) }
129 | TopAppBar(
130 | title = {
131 | if (!showSearch.value) {
132 | Text(text = stringResource(id = R.string.app_name))
133 | } else {
134 | Row(
135 | modifier = Modifier.fillMaxWidth(),
136 | verticalAlignment = Alignment.CenterVertically
137 | ) {
138 | Image(
139 | modifier = Modifier
140 | .clip(CircleShape)
141 | .clickable { showSearch.value = false },
142 | painter = painterResource(id = R.drawable.baseline_arrow_back_24),
143 | contentDescription = stringResource(id = R.string.back),
144 | colorFilter = ColorFilter.tint(Color.White)
145 | )
146 |
147 | var text by remember { mutableStateOf("") }
148 | val selection = TextSelectionColors(
149 | handleColor = LightBlue,
150 | backgroundColor = Color.DarkGray,
151 | )
152 | CompositionLocalProvider(LocalTextSelectionColors provides selection) {
153 | SelectionContainer {
154 | OutlinedTextField(
155 | modifier = Modifier
156 | .fillMaxWidth()
157 | .padding(start = 8.dp, end = 8.dp),
158 | value = text,
159 | onValueChange = {
160 | text = it
161 | updateSearch(it)
162 | },
163 | placeholder = {
164 | Text(text = stringResource(id = R.string.search_with_ellipsis), color = Color.Gray)
165 | },
166 | singleLine = true,
167 | colors = TextFieldDefaults.outlinedTextFieldColors(
168 | unfocusedBorderColor = Color.Transparent,
169 | focusedBorderColor = Color.Transparent,
170 | cursorColor = Color.White)
171 | )
172 | }
173 | }
174 | }
175 | }
176 | },
177 | actions = {
178 | if (!showSearch.value)
179 | Box(
180 | modifier = Modifier
181 | .padding(end = 8.dp)
182 | .size(36.dp)
183 | .clip(CircleShape)
184 | .clickable {
185 | moreShowed = !moreShowed
186 | },
187 | contentAlignment = Alignment.Center
188 | ) {
189 | Column {
190 | Image(
191 | modifier = Modifier.size(28.dp),
192 | painter = painterResource(id = R.drawable.baseline_more_vert_24),
193 | contentDescription = stringResource(id = R.string.settings),
194 | colorFilter = ColorFilter.tint(Color.White)
195 | )
196 | DropdownMenu(
197 | expanded = moreShowed,
198 | onDismissRequest = { moreShowed = false },
199 | ) {
200 | DropdownMenuItem(
201 | onClick = {
202 | showSearch.value = true
203 | moreShowed = false
204 | }
205 | ) {
206 | Text(text = stringResource(id = R.string.search))
207 | }
208 | DropdownMenuItem(
209 | onClick = {
210 | controller.navigate("settings_page")
211 | moreShowed = false
212 | }
213 | ) {
214 | Text(text = stringResource(id = R.string.settings))
215 | }
216 | }
217 | }
218 | }
219 | },
220 | )
221 | }
222 |
223 | @OptIn(ExperimentalMaterialApi::class)
224 | @Composable
225 | private fun NcmFileList() {
226 | val sp = MainActivity.sp
227 |
228 | val refreshScope = rememberCoroutineScope()
229 | var refreshing by remember { mutableStateOf(false) }
230 |
231 | fun refresh() = refreshScope.launch {
232 | refreshing = true
233 | val beg = System.currentTimeMillis()
234 | withContext(Dispatchers.IO) {
235 | if (sp.getBoolean("search_full_disk", false))
236 | reloadFilesFullDisk()
237 | else
238 | reloadFiles()
239 |
240 | listFiltered.clear()
241 | listFiltered += list
242 | }
243 | val end = System.currentTimeMillis()
244 | if (end - beg < 500)
245 | delay(500 - (end - beg))
246 |
247 | refreshing = false
248 | }
249 |
250 | LaunchedEffect("refresh") {
251 | refresh()
252 | }
253 |
254 | val state = rememberPullRefreshState(refreshing, ::refresh)
255 | Box(
256 | modifier = Modifier
257 | .pullRefresh(state)
258 | .background(BackGroundColor)
259 | ) {
260 | val showList = if (showSearch.value)
261 | listFiltered
262 | else
263 | list
264 |
265 | LazyColumn(modifier = Modifier.fillMaxSize()) {
266 | items(showList) { file ->
267 | NcmFileItem(NcmFileInfo(file))
268 | }
269 | }
270 |
271 | if (showList.isEmpty()) {
272 | Text(
273 | text = stringResource(id = R.string.nothing_here),
274 | modifier = Modifier.align(Alignment.Center)
275 | )
276 | }
277 |
278 | PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
279 | }
280 | }
281 |
282 | private val showSearch = mutableStateOf(false)
283 |
284 | @Composable
285 | fun View(controller: NavHostController, activity: MainActivity) {
286 | BackHandler(enabled = showSearch.value) {
287 | showSearch.value = false
288 | }
289 |
290 | Column {
291 | AppBar(controller)
292 | NcmFileList()
293 | }
294 | }
295 | }
296 |
297 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/pages/SettingsPage.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.pages
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Environment
7 | import android.util.Log
8 | import androidx.activity.compose.rememberLauncherForActivityResult
9 | import androidx.activity.result.contract.ActivityResultContracts
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.background
12 | import androidx.compose.foundation.clickable
13 | import androidx.compose.foundation.gestures.Orientation
14 | import androidx.compose.foundation.gestures.scrollable
15 | import androidx.compose.foundation.isSystemInDarkTheme
16 | import androidx.compose.foundation.layout.Arrangement
17 | import androidx.compose.foundation.layout.Box
18 | import androidx.compose.foundation.layout.Column
19 | import androidx.compose.foundation.layout.Row
20 | import androidx.compose.foundation.layout.defaultMinSize
21 | import androidx.compose.foundation.layout.fillMaxSize
22 | import androidx.compose.foundation.layout.fillMaxWidth
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.foundation.layout.statusBarsPadding
25 | import androidx.compose.foundation.rememberScrollState
26 | import androidx.compose.foundation.shape.CircleShape
27 | import androidx.compose.material.MaterialTheme
28 | import androidx.compose.material.Switch
29 | import androidx.compose.material.Text
30 | import androidx.compose.material.TopAppBar
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.setValue
36 | import androidx.compose.ui.Alignment
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.draw.clip
39 | import androidx.compose.ui.graphics.Color
40 | import androidx.compose.ui.graphics.ColorFilter
41 | import androidx.compose.ui.res.painterResource
42 | import androidx.compose.ui.res.stringResource
43 | import androidx.compose.ui.tooling.preview.Preview
44 | import androidx.compose.ui.unit.dp
45 | import androidx.compose.ui.unit.sp
46 | import androidx.navigation.NavHostController
47 | import me.kyuubiran.ncmdumper.BuildConfig
48 | import me.kyuubiran.ncmdumper.MainActivity
49 | import me.kyuubiran.ncmdumper.R
50 | import me.kyuubiran.ncmdumper.ui.theme.BackGroundColor
51 | import me.kyuubiran.ncmdumper.ui.utils.Dumper
52 |
53 | object SettingsPage {
54 | @Composable
55 | private fun AppBar(navController: NavHostController) {
56 | TopAppBar(
57 | title = {
58 | Row {
59 | Image(
60 | modifier = Modifier
61 | .clip(CircleShape)
62 | .clickable { navController.popBackStack() },
63 | painter = painterResource(id = R.drawable.baseline_arrow_back_24),
64 | contentDescription = stringResource(id = R.string.back),
65 | colorFilter = ColorFilter.tint(Color.White)
66 | )
67 | Text(modifier = Modifier.padding(start = 8.dp), text = stringResource(id = R.string.settings))
68 | }
69 | },
70 | )
71 | }
72 |
73 | @Composable
74 | private fun Configs(activity: MainActivity) {
75 | val sp = MainActivity.sp
76 | var outputFolder by remember { mutableStateOf(sp.getString("output_path", Dumper.DEFAULT_NETEASE_MUSIC_PATH)!!) }
77 |
78 | val folderPicker = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
79 | if (it.resultCode == Activity.RESULT_OK) {
80 | val uri = it.data?.data?.path
81 | if (uri != null) {
82 | val realPath = Environment.getExternalStorageDirectory().path + "/" +
83 | uri.substring(uri.indexOf(':') + 1)
84 |
85 | Log.i("SettingsPage", "Set output path: $realPath")
86 | sp.edit().putString("output_path", realPath).apply()
87 | outputFolder = realPath
88 | }
89 | }
90 | }
91 |
92 |
93 | val scrollable = rememberScrollState()
94 | Column(
95 | modifier = Modifier
96 | .fillMaxSize()
97 | .scrollable(state = scrollable, orientation = Orientation.Horizontal)
98 | .background(BackGroundColor)
99 | ) {
100 | // region Dumper
101 | Text(
102 | modifier = Modifier
103 | .padding(start = 8.dp, top = 16.dp, bottom = 4.dp),
104 | text = stringResource(id = R.string.title_dumper),
105 | color = MaterialTheme.colors.primary,
106 | fontSize = 18.sp
107 | )
108 |
109 | var searchFullDiskEnabled by remember { mutableStateOf(sp.getBoolean("search_full_disk", false)) }
110 | SwitchConfigItem(
111 | title = stringResource(id = R.string.title_config_search_full_disk),
112 | subtitle = stringResource(id = R.string.subtitle_config_search_full_disk),
113 | isChecked = searchFullDiskEnabled,
114 | onChanged = {
115 | searchFullDiskEnabled = it
116 | sp.edit().putBoolean("search_full_disk", it).apply()
117 | }
118 | )
119 |
120 | BaseConfigItem(
121 | title = stringResource(id = R.string.title_config_set_output_folder),
122 | subtitle = outputFolder,
123 | onClick = {
124 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
125 | intent.addCategory(Intent.CATEGORY_DEFAULT)
126 | folderPicker.launch(intent)
127 | }
128 | )
129 | // endregion
130 |
131 | // region about
132 | Text(
133 | modifier = Modifier
134 | .padding(start = 8.dp, top = 16.dp, bottom = 4.dp),
135 | text = stringResource(id = R.string.title_about),
136 | color = MaterialTheme.colors.primary,
137 | fontSize = 18.sp
138 | )
139 | BaseConfigItem(
140 | title = stringResource(id = R.string.title_version),
141 | subtitle = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
142 | )
143 | BaseConfigItem(
144 | title = stringResource(id = R.string.title_config_goto_github_page),
145 | subtitle = stringResource(id = R.string.subtitle_config_goto_github_page),
146 | onClick = {
147 | val intent = Intent(Intent.ACTION_VIEW)
148 | intent.data = Uri.parse("https://github.com/KyuubiRan/NcmDumperApp")
149 | activity.startActivity(intent)
150 | }
151 | )
152 | // endregion
153 | }
154 | }
155 |
156 | @Composable
157 | fun View(navController: NavHostController, activity: MainActivity) {
158 | Column {
159 | AppBar(navController = navController)
160 | Configs(activity = activity)
161 | }
162 | }
163 | }
164 |
165 | @Composable
166 | private fun BaseConfigItem(
167 | title: String,
168 | subtitle: String? = null,
169 | onClick: (() -> Unit) = {},
170 | rightView: @Composable () -> Unit = {
171 | Text(text = ">", fontSize = 20.sp, color = Color.Gray)
172 | }
173 | ) {
174 | Box(
175 | modifier = Modifier
176 | .clickable(onClick = onClick)
177 | ) {
178 | Row(
179 | modifier = Modifier.fillMaxWidth()
180 | ) {
181 | Column(
182 | modifier = Modifier
183 | .defaultMinSize(minHeight = 60.dp)
184 | .fillMaxWidth(.8f)
185 | .padding(8.dp),
186 | verticalArrangement = Arrangement.Center
187 | ) {
188 | Text(text = title, fontSize = 18.sp, color = if (isSystemInDarkTheme()) Color.White else Color.Black)
189 | if (!subtitle.isNullOrBlank())
190 | Text(text = subtitle, fontSize = 14.sp, color = Color.Gray)
191 | }
192 |
193 | Box(
194 | modifier = Modifier
195 | .fillMaxWidth()
196 | .align(alignment = Alignment.CenterVertically)
197 | .padding(start = 8.dp),
198 | contentAlignment = Alignment.Center
199 | ) {
200 | rightView()
201 | }
202 | }
203 | }
204 | }
205 |
206 | @Composable
207 | private fun SwitchConfigItem(
208 | title: String,
209 | subtitle: String? = null,
210 | isChecked: Boolean,
211 | onChanged: (Boolean) -> Unit = {},
212 | ) {
213 | BaseConfigItem(
214 | title = title,
215 | subtitle = subtitle,
216 | onClick = { onChanged(!isChecked) },
217 | rightView = {
218 | Switch(checked = isChecked, onCheckedChange = { onChanged(!isChecked) })
219 | }
220 | )
221 | }
222 |
223 | @Preview
224 | @Composable
225 | private fun TestBaseConfigItem() {
226 | Column {
227 | BaseConfigItem(title = "标题", subtitle = "测试", onClick = { })
228 | SwitchConfigItem(title = "测试2", isChecked = true, onChanged = { })
229 | }
230 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 |
7 | val Purple80 = Color(0xFFD0BCFF)
8 | val PurpleGrey80 = Color(0xFFCCC2DC)
9 | val Pink80 = Color(0xFFEFB8C8)
10 |
11 | val Purple40 = Color(0xFF6650a4)
12 | val PurpleGrey40 = Color(0xFF625b71)
13 | val Pink40 = Color(0xFF7D5260)
14 | val LightDark = Color(0xFF1E1E1E)
15 | val LightBlue = Color(0xFF1E88E5)
16 |
17 | val BackGroundColor
18 | @Composable get() = if (isSystemInDarkTheme()) LightDark else Color.White
19 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.Colors
5 | import androidx.compose.material.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.graphics.Color.Companion.Black
8 | import androidx.compose.ui.graphics.Color.Companion.Red
9 | import androidx.compose.ui.graphics.Color.Companion.White
10 | import com.google.accompanist.systemuicontroller.rememberSystemUiController
11 |
12 | private val DarkColorScheme = Colors(
13 | primary = Pink40,
14 | primaryVariant = Pink80,
15 | secondary = PurpleGrey80,
16 | secondaryVariant = PurpleGrey80,
17 | background = Black,
18 | surface = Black,
19 | error = Red,
20 | onPrimary = White,
21 | onSecondary = White,
22 | onBackground = White,
23 | onSurface = White,
24 | onError = White,
25 | isLight = false,
26 | )
27 |
28 | private val LightColorScheme = Colors(
29 | primary = Pink40,
30 | primaryVariant = Pink80,
31 | secondary = PurpleGrey80,
32 | secondaryVariant = PurpleGrey80,
33 | background = White,
34 | surface = White,
35 | error = Red,
36 | onPrimary = White,
37 | onSecondary = White,
38 | onBackground = Black,
39 | onSurface = Black,
40 | onError = White,
41 | isLight = true
42 | )
43 |
44 | @Composable
45 | fun MyTheme(
46 | darkTheme: Boolean = isSystemInDarkTheme(),
47 | // Dynamic color is available on Android 12+
48 | dynamicColor: Boolean = true,
49 | content: @Composable () -> Unit
50 | ) {
51 | val colorScheme = when {
52 | // dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
53 | // val context = LocalContext.current
54 | // if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
55 | // }
56 |
57 | darkTheme -> DarkColorScheme
58 | else -> LightColorScheme
59 | }
60 |
61 |
62 | val systemUiController = rememberSystemUiController()
63 | systemUiController.setSystemBarsColor(
64 | color = if (darkTheme) colorScheme.surface else colorScheme.primary,
65 | )
66 |
67 | MaterialTheme(
68 | colors = colorScheme,
69 | typography = Typography,
70 | content = content
71 | )
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 14.sp,
15 | lineHeight = 20.sp,
16 | letterSpacing = 0.sp
17 | ),
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/utils/DpUtils.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.utils
2 |
3 | import android.content.res.Resources
4 |
5 | private val density = Resources.getSystem().displayMetrics.density
6 |
7 | val Float.px2dp: Float
8 | get() = this / density
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/utils/Dumper.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.utils
2 |
3 | object Dumper {
4 | @JvmStatic
5 | external fun dumpNcmFile(inFilePath: String, outFilePath: String): Int
6 |
7 | const val DEFAULT_NETEASE_MUSIC_PATH = "/storage/emulated/0/netease/cloudmusic/Music"
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/me/kyuubiran/ncmdumper/ui/views/NcmFileItem.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper.ui.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.basicMarquee
7 | import androidx.compose.foundation.clickable
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.material.AlertDialog
15 | import androidx.compose.material.Card
16 | import androidx.compose.material.CircularProgressIndicator
17 | import androidx.compose.material.Text
18 | import androidx.compose.material.TextButton
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableIntStateOf
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.rememberCoroutineScope
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.graphics.Color
29 | import androidx.compose.ui.platform.LocalContext
30 | import androidx.compose.ui.res.stringResource
31 | import androidx.compose.ui.unit.dp
32 | import androidx.compose.ui.unit.sp
33 | import kotlinx.coroutines.Dispatchers
34 | import kotlinx.coroutines.delay
35 | import kotlinx.coroutines.launch
36 | import kotlinx.coroutines.withContext
37 | import me.kyuubiran.ncmdumper.R
38 | import me.kyuubiran.ncmdumper.ui.utils.Dumper.dumpNcmFile
39 | import java.io.File
40 | import java.text.SimpleDateFormat
41 | import java.util.Locale
42 |
43 | @SuppressLint("SimpleDateFormat", "ConstantLocale")
44 | val fmt = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault())
45 |
46 | data class NcmFileInfo(val file: File) {
47 | val fileName: String = file.name
48 | val filePath: String = file.path
49 | val fileSize = file.length()
50 | val createDate: String = fmt.format(file.lastModified())
51 | }
52 |
53 | @Composable
54 | private fun ConfirmDumpDialog(show: Boolean, fileInfo: NcmFileInfo, onDismiss: () -> Unit) {
55 | val sp = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
56 |
57 | var exportResult by remember { mutableIntStateOf(-1) }
58 | var exportingDialogShow by remember { mutableStateOf(false) }
59 | var exportResultDialogShow by remember { mutableStateOf(false) }
60 | val execScope = rememberCoroutineScope()
61 | fun exec() {
62 | execScope.launch {
63 | exportingDialogShow = true
64 | withContext(Dispatchers.IO) {
65 | val beg = System.currentTimeMillis()
66 | val output = sp.getString("output_path", fileInfo.file.parent ?: "") ?: ""
67 | exportResult = dumpNcmFile(fileInfo.filePath, output)
68 | val end = System.currentTimeMillis()
69 | if (end - beg < 500) delay(500 - (end - beg))
70 | }
71 | exportingDialogShow = false
72 | exportResultDialogShow = true
73 | }
74 | }
75 |
76 | if (show)
77 | AlertDialog(onDismissRequest = onDismiss,
78 | buttons = {
79 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
80 | TextButton(onClick = onDismiss) {
81 | Text(text = stringResource(id = R.string.cancel))
82 | }
83 | TextButton(onClick = {
84 | exec()
85 | onDismiss()
86 | }) {
87 | Text(text = stringResource(id = R.string.confirm))
88 | }
89 | }
90 | },
91 | title = {
92 | Text(text = stringResource(id = R.string.title_dialog_confirm_dump))
93 | },
94 | text = {
95 | Text(
96 | text = stringResource(id = R.string.message_dialog_confirm_dump).format(fileInfo.fileName)
97 | )
98 | })
99 |
100 | if (exportingDialogShow)
101 | AlertDialog(
102 | onDismissRequest = { },
103 | buttons = {
104 | Box(
105 | modifier = Modifier.padding(16.dp),
106 | contentAlignment = Alignment.Center
107 | ) {
108 | Column {
109 | CircularProgressIndicator()
110 | Text(
111 | modifier = Modifier.padding(top = 8.dp),
112 | text = stringResource(id = R.string.message_dialog_exporting)
113 | )
114 | }
115 | }
116 | }
117 | )
118 |
119 | if (exportResultDialogShow)
120 | AlertDialog(onDismissRequest = { exportResultDialogShow = false },
121 | buttons = {
122 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
123 | TextButton(onClick = { exportResultDialogShow = false }) {
124 | Text(text = stringResource(id = R.string.confirm))
125 | }
126 | }
127 | },
128 | title = {
129 | Text(text = stringResource(id = if (exportResult == 0) R.string.title_dialog_dump_success else R.string.title_dialog_dump_failed))
130 | },
131 | text = {
132 | val resId = when (exportResult) {
133 | 0 -> R.string.message_dialog_dump_success
134 | 1 -> R.string.message_dialog_dump_failed_invalid_input_file
135 | 2 -> R.string.message_dialog_dump_failed_invalid_output_folder
136 | 3 -> R.string.message_dialog_dump_failed_file_not_ncm_file
137 | 4 -> R.string.message_dialog_dump_failed_file_unknow_format
138 | 5 -> R.string.message_dialog_dump_failed_file_cannot_read_music_info
139 | 6 -> R.string.message_dialog_dump_failed_file_cannot_read_music_cover
140 | 7 -> R.string.message_dialog_dump_failed_file_cannot_read_music_data
141 | 8 -> R.string.message_dialog_dump_failed_file_cannot_save_output_file
142 | else -> R.string.message_dialog_dump_failed_unknown
143 | }
144 | Text(text = stringResource(id = resId))
145 | })
146 | }
147 |
148 | @OptIn(ExperimentalFoundationApi::class)
149 | @Composable
150 | fun NcmFileItem(fileInfo: NcmFileInfo) {
151 | var dialogShow by remember { mutableStateOf(false) }
152 | ConfirmDumpDialog(show = dialogShow, fileInfo = fileInfo) {
153 | dialogShow = false
154 | }
155 |
156 | Card(
157 | modifier = Modifier
158 | .fillMaxWidth()
159 | .padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 2.dp),
160 | ) {
161 | Box(modifier = Modifier.clickable { dialogShow = true }) {
162 | Column(
163 | modifier = Modifier
164 | .padding(8.dp),
165 | verticalArrangement = Arrangement.Center
166 | ) {
167 | Text(
168 | maxLines = 1,
169 | modifier = Modifier.basicMarquee(),
170 | text = fileInfo.fileName,
171 | fontSize = 18.sp
172 | )
173 | Text(
174 | maxLines = 1,
175 | text = "${fileInfo.createDate} | ${String.format("%.2f", fileInfo.fileSize / 1024.00 / 1024.00)}MB",
176 | color = Color.Gray,
177 | fontSize = 14.sp
178 | )
179 | }
180 | }
181 | }
182 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_more_vert_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_refresh_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ncm Dumper
4 | 设置
5 | 确认
6 | 取消
7 | 授权访问所有文件
8 | 授予访问所有文件的权限来获取.ncm文件列表
9 | 刷新
10 | 导出歌曲
11 | 确认导出歌曲 %s ?
12 | 导出中…
13 | 导出成功
14 | 导出失败
15 | 已成功导出歌曲。
16 | 错误:无效的输入流!
17 | 错误:无效的输出文件夹!
18 | 错误:Ncm文件头校验失败,此文件可能不是.ncm格式的文件!
19 | 错误:未知的音乐格式!
20 | 错误:无法读取歌曲信息!
21 | 错误:无法读取歌曲封面信息!
22 | 错误:无法读取歌曲数据!
23 | 错误:无法保存输出文件!
24 | 错误:未知错误!
25 | 空空如也~
26 | 搜索
27 | 返回
28 | 设置输出目录
29 | 全盘搜索
30 | 全盘搜索.ncm文件,否则仅搜索网易云音乐的下载目录。
31 | 导出
32 | 关于
33 | 版本
34 | 前往Github浏览项目
35 | 点个Star吧 :)
36 | 搜索…
37 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Ncm Dumper
3 | Settings
4 | Confirm
5 | Cancel
6 | Require all file access permission
7 | Grant all file access permisson to find .ncm files
8 | Refresh
9 | Dump music
10 | Do you want to dump music %s ?
11 | Dumping…
12 | Dump succeed
13 | Dump failed
14 | Dump music file succeed.
15 | Error: Invalid input stream!
16 | Error: Invalid output folder
17 | Error: Ncm header check failed, this file may not .ncm file!
18 | Error: Unknown music file format!
19 | Error: Cannot read music info!
20 | Error: Cannot read music cover data!
21 | Error: Cannot read music data!
22 | Error: Cannot save output file!
23 | Error: Unknown error!
24 | Nothing here~
25 | Search
26 | Search…
27 | Back
28 | Set output folder
29 | Search full disk
30 | Search .ncm files in the storage, otherwise only search netease cloud music download dir.
31 | Dumper
32 | About
33 | Version
34 | View project on github
35 | Star me plz :)
36 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/me/kyuubiran/ncmdumper/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package me.kyuubiran.ncmdumper
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.androidApplication) apply false
4 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false
5 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | accompanistSystemuicontroller = "0.32.0"
3 | agp = "8.3.0-beta01"
4 | kotlin = "1.9.0"
5 | coreKtx = "1.12.0"
6 | junit = "4.13.2"
7 | junitVersion = "1.1.5"
8 | espressoCore = "3.5.1"
9 | lifecycleRuntimeKtx = "2.6.2"
10 | activityCompose = "1.8.2"
11 | composeBom = "2023.12.00-alpha04"
12 | navigationCompose = "2.7.6"
13 |
14 | [libraries]
15 | accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
16 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
17 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
18 | junit = { group = "junit", name = "junit", version.ref = "junit" }
19 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
20 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
21 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
22 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
23 | androidx-compose-bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "composeBom" }
24 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
25 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
26 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
27 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
28 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
29 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
30 | androidx-material = { group = "androidx.compose.material", name = "material" }
31 |
32 | [plugins]
33 | androidApplication = { id = "com.android.application", version.ref = "agp" }
34 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
35 |
36 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KyuubiRan/NcmDumperApp/285b125e385dd2fdb6bc4f68eecaafb7e6a6907f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Dec 30 22:41:47 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "NcmDumper"
23 | include(":app")
24 |
--------------------------------------------------------------------------------