@@ -11,18 +11,15 @@ import java.util.zip.ZipInputStream
1111/* *
1212 * Installs the small system-emotion resource set that NT expects under filesDir.
1313 *
14- * The official resource manager does not read these files from APK assets. It only looks at
15- * filesDir/qq_emoticon_res, so the bridge materializes the bundled config and lottie/static files
16- * before the manager is initialized. Existing downloaded resources are preserved.
14+ * Bundles [face_config.json] and [emoji_res.zip] only. Animated system faces are served
15+ * on-demand from [QFaceRemoteStore] instead of a packaged bigface/qlottie zip.
1716 */
1817object EmotionAssetBridge {
1918 private const val TAG = " QMCE-EmotionAssets"
20- private const val VERSION = 7
19+ private const val VERSION = 9
2120 private const val RESOURCE_DIR = " qq_emoticon_res"
2221 private const val CONFIG_ASSET = " face_config.json"
23- private const val ZIP_ASSET = " bigface.zip"
2422 private const val EMOJI_ZIP_ASSET = " emoji_res.zip"
25- private const val INDEX_ASSET = " bigface_index.json"
2623 private const val MARKER = " .qmce_emotion_assets_v$VERSION "
2724
2825 @Volatile
@@ -35,7 +32,7 @@ object EmotionAssetBridge {
3532 synchronized(this ) {
3633 applicationContext = context.applicationContext
3734 runCatching {
38- ensureLocked(context)
35+ ensureLocked(context.applicationContext )
3936 }.onFailure {
4037 Log .e(TAG , " emotion resource initialization failed" , it)
4138 }
@@ -59,31 +56,26 @@ object EmotionAssetBridge {
5956
6057 private fun ensureFiles (context : Context , resourceDir : File ) {
6158 val bundledConfig = context.assets.open(CONFIG_ASSET ).bufferedReader().use { it.readText() }
62- val bundledIndex = context.assets.open(INDEX_ASSET ).bufferedReader().use { it.readText() }
63- val indexFile = File (resourceDir, INDEX_ASSET )
64- if (! indexFile.isFile || indexFile.readText() != bundledIndex) {
65- writeAtomically(indexFile, bundledIndex)
66- }
59+ val animatedIds = QFaceRemoteStore .animatedIds().map(Int ::toString)
6760 val mergedConfig = mergeConfig(
6861 existing = File (resourceDir, CONFIG_ASSET ).takeIf { it.isFile }?.readText(),
6962 bundled = bundledConfig,
70- animatedIds = readAnimatedIds(bundledIndex) ,
63+ animatedIds = animatedIds ,
7164 )
7265 val configFile = File (resourceDir, CONFIG_ASSET )
7366 if (! configFile.isFile || configFile.readText() != mergedConfig) {
7467 writeAtomically(configFile, mergedConfig)
7568 }
7669
7770 val marker = File (resourceDir, MARKER )
78- if (! marker.isFile || ! hasBundledResource(resourceDir, bundledIndex, bundledConfig)) {
79- extractZipAsset(context, resourceDir, ZIP_ASSET )
80- extractZipAsset(context, resourceDir, EMOJI_ZIP_ASSET )
71+ if (! marker.isFile || ! hasStaticBundledResource(resourceDir, bundledConfig)) {
72+ extractZipAssetDirect(context, resourceDir, EMOJI_ZIP_ASSET )
8173 writeAtomically(marker, " version=$VERSION \n " )
8274 }
8375 Log .i(
8476 TAG ,
8577 " emotion resources ready config=${configFile.length()} bytes " +
86- " animated=${readAnimatedIds(bundledIndex) .size} dir=${resourceDir.absolutePath} " ,
78+ " animated=${animatedIds .size} dir=${resourceDir.absolutePath} " ,
8779 )
8880 }
8981
@@ -98,10 +90,7 @@ object EmotionAssetBridge {
9890 }.getOrNull()
9991 }
10092
101- fun bundledAnimatedIds (): List <Int > = runCatching {
102- val index = resourceFile(INDEX_ASSET )?.takeIf (File ::isFile)?.readText() ? : return @runCatching emptyList()
103- readAnimatedIds(index).mapNotNull(String ::toIntOrNull).distinct()
104- }.getOrDefault(emptyList())
93+ fun bundledAnimatedIds (): List <Int > = QFaceRemoteStore .animatedIds()
10594
10695 private fun mergeConfig (
10796 existing : String? ,
@@ -141,6 +130,7 @@ object EmotionAssetBridge {
141130 }
142131
143132 animatedIds.forEach { id ->
133+ val entry = QFaceRemoteStore .lookup(id)
144134 if (! itemIndexById.containsKey(id)) {
145135 itemIndexById[id] = baseSysface.length()
146136 baseSysface.put(
@@ -149,10 +139,19 @@ object EmotionAssetBridge {
149139 put(" IQLid" , id)
150140 put(" AQLid" , id)
151141 put(" EMCode" , " 10$id " )
152- put(" QDes" , " /大表情$id " )
142+ put(
143+ " QDes" ,
144+ entry?.describe?.takeIf { it.isNotBlank() } ? : " /大表情$id " ,
145+ )
153146 put(" AniStickerType" , 1 )
154- put(" AniStickerPackId" , " 1" )
155- put(" AniStickerId" , id)
147+ put(
148+ " AniStickerPackId" ,
149+ entry?.aniStickerPackId?.takeIf { it > 0 }?.toString() ? : " 1" ,
150+ )
151+ put(
152+ " AniStickerId" ,
153+ entry?.aniStickerId?.takeIf { it > 0 }?.toString() ? : id,
154+ )
156155 },
157156 )
158157 }
@@ -177,86 +176,40 @@ object EmotionAssetBridge {
177176 }
178177 }
179178
180- private fun readAnimatedIds (indexJson : String ): List <String > = runCatching {
181- val ids = JSONObject (indexJson).optJSONArray(" ids" ) ? : return @runCatching emptyList()
182- buildList(ids.length()) {
183- for (index in 0 until ids.length()) {
184- ids.optString(index).takeIf (String ::isNotBlank)?.let (::add)
185- }
186- }.distinct()
187- }.getOrDefault(emptyList())
188-
189- private fun hasBundledResource (
179+ private fun hasStaticBundledResource (
190180 resourceDir : File ,
191- indexJson : String ,
192181 bundledConfigJson : String ,
193182 ): Boolean {
194- val indexIds = readAnimatedIds(indexJson)
195- val configIds = runCatching {
196- val sysface = JSONObject (bundledConfigJson).optJSONArray(" sysface" ) ? : return @runCatching emptyList()
197- buildList {
198- for (index in 0 until sysface.length()) {
199- val item = sysface.optJSONObject(index) ? : continue
200- if (item.optInt(" AniStickerType" , 0 ) <= 0 ) continue
201- item.optString(" AniStickerId" )
202- .takeIf (String ::isNotBlank)
203- ?.let (::add)
204- }
205- }
206- }.getOrDefault(emptyList())
207- val animatedIds = (indexIds + configIds).distinct()
208- val animatedReady = animatedIds.isEmpty() || animatedIds.all { id ->
209- File (resourceDir, " qlottie/1/$id /$id .json" ).isFile
210- }
211183 val configReady = runCatching {
212184 JSONObject (bundledConfigJson).optJSONArray(" sysface" )?.length()?.let { it > 0 } == true &&
213185 File (resourceDir, CONFIG_ASSET ).isFile
214186 }.getOrDefault(false )
215187 val emojiReady = (0 .. 164 ).all { index ->
216188 File (resourceDir, " emoji_res/emoji_${index.toString().padStart(3 , ' 0' )} .png" ).isFile
217189 }
218- return animatedReady && configReady && emojiReady
190+ return configReady && emojiReady
219191 }
220192
221- private fun extractZipAsset (context : Context , resourceDir : File , assetName : String ) {
222- val stage = File (
223- resourceDir.parentFile,
224- " .${resourceDir.name} .qmce-stage-${System .nanoTime()} " ,
225- )
226- deleteRecursively(stage)
227- if (! stage.mkdirs()) error(" cannot create ${stage.absolutePath} " )
228-
193+ private fun extractZipAssetDirect (context : Context , resourceDir : File , assetName : String ): Int {
229194 var extracted = 0
230- try {
231- context.assets.open(assetName).buffered().use { input ->
232- ZipInputStream (input).use { zip ->
233- while (true ) {
234- val entry = zip.nextEntry ? : break
235- val output = safeEntry(stage, entry.name)
236- if (entry.isDirectory) {
237- output.mkdirs()
238- } else {
239- output.parentFile?.mkdirs()
240- output.outputStream().buffered().use { zip.copyTo(it) }
241- extracted++
242- }
243- zip.closeEntry()
195+ context.assets.open(assetName).buffered().use { input ->
196+ ZipInputStream (input).use { zip ->
197+ while (true ) {
198+ val entry = zip.nextEntry ? : break
199+ if (entry.isDirectory) {
200+ safeEntry(resourceDir, entry.name).mkdirs()
201+ } else {
202+ val output = safeEntry(resourceDir, entry.name)
203+ output.parentFile?.mkdirs()
204+ output.outputStream().buffered().use { zip.copyTo(it) }
205+ extracted++
244206 }
207+ zip.closeEntry()
245208 }
246209 }
247-
248- stage.walkTopDown()
249- .filter(File ::isFile)
250- .forEach { source ->
251- val relative = source.relativeTo(stage)
252- val target = File (resourceDir, relative.path)
253- target.parentFile?.mkdirs()
254- source.copyTo(target, overwrite = true )
255- }
256- Log .i(TAG , " extracted $extracted bundled resources from $assetName " )
257- } finally {
258- deleteRecursively(stage)
259210 }
211+ Log .i(TAG , " extracted $extracted entries from $assetName -> ${resourceDir.absolutePath} " )
212+ return extracted
260213 }
261214
262215 private fun safeEntry (root : File , entryName : String ): File {
@@ -275,9 +228,4 @@ object EmotionAssetBridge {
275228 temporary.delete()
276229 }
277230 }
278-
279- private fun deleteRecursively (file : File ) {
280- if (! file.exists()) return
281- file.walkBottomUp().forEach { it.delete() }
282- }
283231}
0 commit comments