Compare commits
35 Commits
v0.1.0-dav
...
6cd2a22d09
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cd2a22d09 | ||
|
|
71f6b6369b | ||
|
|
e5ce52ac40 | ||
|
|
a2f204f5ee | ||
|
|
eeea966400 | ||
|
|
d68e6ca6df | ||
|
|
a96560966c | ||
|
|
ffd05e3e17 | ||
|
|
40d0c86292 | ||
|
|
e8f00faf41 | ||
|
|
b70c5015a0 | ||
|
|
14367a229b | ||
|
|
e02997db52 | ||
|
|
82a0650b59 | ||
|
|
0d3fa632e6 | ||
|
|
ad1b3e3aca | ||
|
|
0f37573faf | ||
|
|
49d3c187d0 | ||
|
|
3da606671a | ||
|
|
1461acd292 | ||
|
|
a7b6b455d1 | ||
|
|
c62e2a4430 | ||
|
|
20d81d5ab8 | ||
|
|
c1849802b6 | ||
|
|
31a56708d6 | ||
|
|
1b3d2fc06f | ||
|
|
db5b7de50f | ||
|
|
3244876c8c | ||
|
|
fcc51f4356 | ||
|
|
70e39454df | ||
|
|
d4169538c9 | ||
|
|
a3f0f975a6 | ||
|
|
109ad813e6 | ||
|
|
34cdec130f | ||
|
|
eab872ff09 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -39,9 +39,11 @@ src-tauri/target/
|
|||||||
|
|
||||||
# Android
|
# Android
|
||||||
android/app/build/
|
android/app/build/
|
||||||
android/.gradle/
|
**/android/.gradle/
|
||||||
android/build/
|
android/build/
|
||||||
*.hprof
|
*.hprof
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
local.properties
|
local.properties
|
||||||
|
screen.png
|
||||||
|
|
||||||
|
|||||||
8
.reasonix/desktop-topic-auto-title-meta.json
Normal file
8
.reasonix/desktop-topic-auto-title-meta.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": {
|
||||||
|
"stage": 3,
|
||||||
|
"userTurns": 3,
|
||||||
|
"basisHash": "942c58e1a7d2413a",
|
||||||
|
"updatedAt": 1784389797592
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-created-at.json
Normal file
3
.reasonix/desktop-topic-created-at.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": 1784388460704
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-title-sources.json
Normal file
3
.reasonix/desktop-topic-title-sources.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": "auto"
|
||||||
|
}
|
||||||
3
.reasonix/desktop-topic-titles.json
Normal file
3
.reasonix/desktop-topic-titles.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"topic_20260718-152740_2cab7b15ea20700d": "Start pursuing the…"
|
||||||
|
}
|
||||||
53
analyze_text.py
Normal file
53
analyze_text.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""
|
||||||
|
Final analysis: Search for any readable content in the decoded text.
|
||||||
|
Look for patterns that might indicate it's a screenshot from an Android device.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
text = raw.decode('utf-16-le')
|
||||||
|
|
||||||
|
# Save full text for manual inspection
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_full_text.txt", 'w', encoding='utf-8') as f:
|
||||||
|
f.write(text)
|
||||||
|
print("Full text written to screen_full_text.txt")
|
||||||
|
|
||||||
|
# Search for any CJK character sequences (potential readable Chinese text)
|
||||||
|
# Chinese characters are in range U+4E00 to U+9FFF
|
||||||
|
cjk_pattern = re.compile(r'[\u4e00-\u9fff]{3,}')
|
||||||
|
cjk_matches = cjk_pattern.findall(text)
|
||||||
|
print(f"\nChinese character sequences (>=3 chars): {len(cjk_matches)}")
|
||||||
|
# Show unique ones
|
||||||
|
unique_cjk = list(set(cjk_matches))
|
||||||
|
print(f"Unique sequences: {len(unique_cjk)}")
|
||||||
|
for seq in sorted(unique_cjk, key=len, reverse=True)[:30]:
|
||||||
|
print(f" '{seq}' (len={len(seq)})")
|
||||||
|
|
||||||
|
# Search for any ASCII word sequences (potential English words)
|
||||||
|
# Words of length >= 4 made of letters only
|
||||||
|
word_pattern = re.compile(r'[A-Za-z]{4,}')
|
||||||
|
word_matches = word_pattern.findall(text)
|
||||||
|
print(f"\nEnglish word-like sequences (>=4 chars): {len(word_matches)}")
|
||||||
|
unique_words = list(set(word_matches))
|
||||||
|
print(f"Unique: {len(unique_words)}")
|
||||||
|
for w in sorted(unique_words, key=len, reverse=True)[:40]:
|
||||||
|
print(f" '{w}'")
|
||||||
|
|
||||||
|
# Also search for numbers that look like versions
|
||||||
|
version_pattern = re.compile(r'\d+\.\d+(\.\d+)?')
|
||||||
|
version_matches = version_pattern.findall(text)
|
||||||
|
print(f"\nVersion-like patterns: {len(version_matches)}")
|
||||||
|
for v in version_matches[:20]:
|
||||||
|
print(f" '{v}'")
|
||||||
|
|
||||||
|
# Search for URLs or file paths
|
||||||
|
path_pattern = re.compile(r'[/\\][A-Za-z0-9_./\\-]{5,}')
|
||||||
|
path_matches = path_pattern.findall(text)
|
||||||
|
print(f"\nPath-like patterns: {len(path_matches)}")
|
||||||
|
for p in path_matches[:20]:
|
||||||
|
print(f" '{p}'")
|
||||||
17
check_files.py
Normal file
17
check_files.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
# Check screen2.png
|
||||||
|
data = open(r"D:\Yuzu-GCA\screen2.png", 'rb').read()
|
||||||
|
print(f"screen2.png: Size={len(data)}")
|
||||||
|
print(f"First 20 bytes: {' '.join(f'{b:02X}' for b in data[:20])}")
|
||||||
|
print(f"Is valid PNG: {data[:8] == b'\x89PNG\r\n\x1a\n'}")
|
||||||
|
|
||||||
|
# Also check the fixed files
|
||||||
|
for fname in ['screen_fixed.png', 'screen_fixed2.png']:
|
||||||
|
try:
|
||||||
|
d = open(r"D:\\Yuzu-GCA\\" + fname, 'rb').read()
|
||||||
|
print(f"\n{fname}: Size={len(d)}")
|
||||||
|
except:
|
||||||
|
print(f"\n{fname}: NOT FOUND")
|
||||||
36
check_header.py
Normal file
36
check_header.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
image_path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
header = f.read(32)
|
||||||
|
|
||||||
|
print(f"File size check: first 32 bytes hex:")
|
||||||
|
print(' '.join(f'{b:02X}' for b in header))
|
||||||
|
print()
|
||||||
|
print(f"ASCII representation: {header}")
|
||||||
|
|
||||||
|
# Check known magic numbers
|
||||||
|
if header[:8] == b'\x89PNG\r\n\x1a\n':
|
||||||
|
print("✓ Valid PNG header detected")
|
||||||
|
elif header[:2] == b'\xff\xd8':
|
||||||
|
print("✓ JPEG header detected")
|
||||||
|
elif header[:4] == b'BM':
|
||||||
|
print("✓ BMP header detected")
|
||||||
|
elif header[:4] == b'RIFF':
|
||||||
|
print("✓ WEBP header detected")
|
||||||
|
else:
|
||||||
|
print("⚠ Unknown image format")
|
||||||
|
|
||||||
|
# Also check if it might be an Android screencap (could be raw)
|
||||||
|
# Try to detect if it's a PNG embedded in a wrapper
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check if file is actually a text-based format
|
||||||
|
with open(image_path, 'rb') as f:
|
||||||
|
content = f.read(200)
|
||||||
|
# Check if mostly printable ASCII
|
||||||
|
printable = sum(1 for b in content if 32 <= b <= 126 or b in (9, 10, 13))
|
||||||
|
if printable > len(content) * 0.8:
|
||||||
|
print("Note: File appears to be mostly text!")
|
||||||
|
print(content.decode('utf-8', errors='replace'))
|
||||||
74
check_png.py
Normal file
74
check_png.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
print(f"Total size: {len(data)}")
|
||||||
|
|
||||||
|
# Check PNG structure
|
||||||
|
# Signature: 8 bytes
|
||||||
|
# Then chunks: each chunk has 4-byte length, 4-byte type, data, 4-byte CRC
|
||||||
|
|
||||||
|
offset = 8 # skip signature
|
||||||
|
|
||||||
|
while offset < len(data):
|
||||||
|
if offset + 8 > len(data):
|
||||||
|
break
|
||||||
|
length = int.from_bytes(data[offset:offset+4], 'big')
|
||||||
|
chunk_type = data[offset+4:offset+8].decode('ascii', errors='replace')
|
||||||
|
print(f"Chunk at offset {offset}: type='{chunk_type}', length={length}")
|
||||||
|
|
||||||
|
if chunk_type == 'IHDR':
|
||||||
|
w = int.from_bytes(data[offset+8:offset+12], 'big')
|
||||||
|
h = int.from_bytes(data[offset+12:offset+16], 'big')
|
||||||
|
bit_depth = data[offset+16]
|
||||||
|
color_type = data[offset+17]
|
||||||
|
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
|
||||||
|
# Height seems wrong (854591). Expected ~1920 or ~2340
|
||||||
|
# 0x000D0A3F: maybe it was 0x00000780 (1920) or 0x00000924 (2340)?
|
||||||
|
# Let me look at the raw bytes around height
|
||||||
|
print(f" Raw height bytes: {' '.join(f'{b:02X}' for b in data[offset+12:offset+16])}")
|
||||||
|
# Expected height around 1920 = 0x780, or 2340 = 0x924
|
||||||
|
# What we have: 00 0D 0A 3F = 0x000D0A3F
|
||||||
|
# Could it be that 0D 0A is an inserted CR LF?
|
||||||
|
# If we remove 0D 0A: remaining would be 00 3F... but that's only 2 bytes
|
||||||
|
# Actually: 00 [0D 0A] 3F -> 00 3F? That's too short.
|
||||||
|
# Maybe: 00 [0D] 0A 3F, and 0D was inserted? Then original: 00 0A 3F = 2623 pixels?
|
||||||
|
# Or maybe: 00 00 0A 3F was original? No.
|
||||||
|
|
||||||
|
# Let me check: common heights: 1920=0x780, 2000=0x7D0, 2340=0x924
|
||||||
|
# 0x0A3F = 2623 (close to nothing standard)
|
||||||
|
# What if the actual height bytes are 00 00 0A 3F? That's still 2623.
|
||||||
|
# What about 00 00 07 80 (1920)? Then 0D 0A replaced 00 07 somehow?
|
||||||
|
|
||||||
|
offset += 12 + length
|
||||||
|
|
||||||
|
if offset > 200:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Let me search for CR LF (0D 0A) in the PNG data
|
||||||
|
crlf_count = 0
|
||||||
|
for i in range(len(data) - 1):
|
||||||
|
if data[i] == 0x0D and data[i+1] == 0x0A:
|
||||||
|
crlf_count += 1
|
||||||
|
if crlf_count <= 10:
|
||||||
|
print(f"CR LF at offset {i}: context: {' '.join(f'{b:02X}' for b in data[max(0,i-4):i+6])}")
|
||||||
|
|
||||||
|
print(f"\nTotal CR LF sequences: {crlf_count}")
|
||||||
|
|
||||||
|
# Also count lone 0A
|
||||||
|
lone_lf = 0
|
||||||
|
for i in range(len(data)):
|
||||||
|
if data[i] == 0x0A and (i == 0 or data[i-1] != 0x0D):
|
||||||
|
lone_lf += 1
|
||||||
|
print(f"Lone LF: {lone_lf}")
|
||||||
|
|
||||||
|
# Count lone 0D
|
||||||
|
lone_cr = 0
|
||||||
|
for i in range(len(data)):
|
||||||
|
if data[i] == 0x0D and (i == len(data)-1 or data[i+1] != 0x0A):
|
||||||
|
lone_cr += 1
|
||||||
|
print(f"Lone CR: {lone_cr}")
|
||||||
1
chi_sim.traineddata
Normal file
1
chi_sim.traineddata
Normal file
@@ -0,0 +1 @@
|
|||||||
|
File size exceeded the configured limit of 20 MB.
|
||||||
82
debug_png.py
Normal file
82
debug_png.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
Debug: compare raw bytes and re-encoded bytes.
|
||||||
|
Also try to interpret the file as a regular PNG by skipping the BOM.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
print(f"Total size: {len(raw)} bytes")
|
||||||
|
|
||||||
|
# Decode as UTF-16 LE
|
||||||
|
text = raw.decode('utf-16-le')
|
||||||
|
print(f"Decoded text length: {len(text)} chars")
|
||||||
|
|
||||||
|
# Re-encode
|
||||||
|
reencoded = text.encode('utf-16-le')
|
||||||
|
print(f"Re-encoded size: {len(reencoded)} bytes")
|
||||||
|
|
||||||
|
# Compare
|
||||||
|
if raw == reencoded:
|
||||||
|
print("raw == reencoded: EXACT MATCH")
|
||||||
|
else:
|
||||||
|
print(f"raw == reencoded: DIFFER")
|
||||||
|
# Find first difference
|
||||||
|
for i in range(min(len(raw), len(reencoded))):
|
||||||
|
if raw[i] != reencoded[i]:
|
||||||
|
print(f"First diff at byte {i}: raw={raw[i]:02x}, reencoded={reencoded[i]:02x}")
|
||||||
|
print(f" Context raw: {raw[max(0,i-5):i+10].hex(' ')}")
|
||||||
|
print(f" Context re: {reencoded[max(0,i-5):i+10].hex(' ')}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check if raw starts with BOM
|
||||||
|
if raw[:2] == b'\xff\xfe':
|
||||||
|
print("\nFile starts with UTF-16 LE BOM")
|
||||||
|
elif raw[:2] == b'\xfe\xff':
|
||||||
|
print("\nFile starts with UTF-16 BE BOM")
|
||||||
|
|
||||||
|
# Check reencoded start
|
||||||
|
if reencoded[:2] == b'\xff\xfe':
|
||||||
|
print("Re-encoded starts with UTF-16 LE BOM")
|
||||||
|
elif reencoded[:2] == b'\xfe\xff':
|
||||||
|
print("Re-encoded starts with UTF-16 BE BOM")
|
||||||
|
|
||||||
|
# Try: what if we treat raw[2:] as the UTF-16 LE data (without the BOM)?
|
||||||
|
# This would be the case if the file had BOM added later
|
||||||
|
print(f"\nraw[2:4] = {raw[2:4].hex(' ')}")
|
||||||
|
text2 = raw[2:].decode('utf-16-le')
|
||||||
|
print(f"Decoded text2 (skipping BOM) length: {len(text2)} chars")
|
||||||
|
print(f"text2 first 50 chars: {text2[:50]}")
|
||||||
|
|
||||||
|
# Try: what if the entire file is just raw PNG with a bogus BOM?
|
||||||
|
# PNG magic: 89 50 4E 47 0D 0A 1A 0A
|
||||||
|
# Our file: ff fe 52 58 4e 00 47 00 0d 00 0a 00 ...
|
||||||
|
# Maybe each byte X became X*256 + something?
|
||||||
|
# Or maybe the file is a hex dump?
|
||||||
|
print("\n=== Trying hex dump interpretation ===")
|
||||||
|
# Check if the text looks like a hex dump (pairs of hex digits)
|
||||||
|
hex_pattern = all(c in '0123456789abcdefABCDEF \n\r\t' for c in text[:1000])
|
||||||
|
print(f"First 1000 chars look like hex dump: {hex_pattern}")
|
||||||
|
|
||||||
|
# Final: Is there any actual readable content?
|
||||||
|
# Let's look for sequences of Latin chars that might form words
|
||||||
|
import re
|
||||||
|
# Words with 5+ letters
|
||||||
|
real_words = re.findall(r'[A-Za-z]{5,}', text)
|
||||||
|
# Filter out those that look like random binary
|
||||||
|
likely_real = [w for w in real_words if w.lower() in [
|
||||||
|
'update', 'download', 'install', 'error', 'success', 'failed',
|
||||||
|
'system', 'android', 'version', 'checking', 'package', 'verify'
|
||||||
|
] or w in ['IHDR', 'IDAT', 'IEND', 'sRGB', 'sBIT', 'pHYs', 'iTXt', 'tEXt', 'zTXt']]
|
||||||
|
print(f"\nKnown words found: {likely_real}")
|
||||||
|
|
||||||
|
# Check for PNG chunk names
|
||||||
|
png_chunks = ['IHDR', 'PLTE', 'IDAT', 'IEND', 'cHRM', 'gAMA', 'iCCP', 'sBIT', 'sRGB',
|
||||||
|
'bKGD', 'hIST', 'tRNS', 'pHYs', 'sPLT', 'tIME', 'iTXt', 'tEXt', 'zTXt']
|
||||||
|
for chunk in png_chunks:
|
||||||
|
cnt = text.count(chunk)
|
||||||
|
if cnt > 0:
|
||||||
|
print(f" PNG chunk '{chunk}': {cnt} times")
|
||||||
15
decode_png.py
Normal file
15
decode_png.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
# Set stdout to UTF-8
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
print(f"Total bytes: {len(data)}")
|
||||||
|
|
||||||
|
# Try UTF-16 LE decode
|
||||||
|
text = data.decode('utf-16-le', errors='replace')
|
||||||
|
print("=== Decoded as UTF-16 LE (first 10000 chars) ===")
|
||||||
|
print(text[:10000])
|
||||||
129
fix_png.py
Normal file
129
fix_png.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
with open(r"D:\Yuzu-GCA\screen.png", 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
# The file is UTF-16 LE encoded with some corruption.
|
||||||
|
# Let's fix the known issues:
|
||||||
|
|
||||||
|
# 1. Replace the corrupted first char (U+5852 from bytes 52 58)
|
||||||
|
# with correct U+0089 (89 00) + U+0050 (50 00)
|
||||||
|
# But wait - in UTF-16 LE, each char takes 2 bytes.
|
||||||
|
# We need to replace 2 bytes (52 58) with 4 bytes (89 00 50 00)
|
||||||
|
|
||||||
|
# Actually, let's approach differently:
|
||||||
|
# Convert the whole thing from UTF-16 LE to raw bytes,
|
||||||
|
# but fix the known corruptions.
|
||||||
|
|
||||||
|
# Step 1: parse as UTF-16 LE (skip BOM)
|
||||||
|
raw = data[2:]
|
||||||
|
# Each 2 bytes is one UTF-16 LE char
|
||||||
|
chars = []
|
||||||
|
for i in range(0, len(raw), 2):
|
||||||
|
if i + 1 < len(raw):
|
||||||
|
ch = raw[i] | (raw[i+1] << 8)
|
||||||
|
chars.append(ch)
|
||||||
|
|
||||||
|
print(f"Total chars: {len(chars)}")
|
||||||
|
|
||||||
|
# Step 2: Fix corruptions in the char array
|
||||||
|
# Char 0: U+5852 -> should be U+0089 (first PNG byte)
|
||||||
|
# But U+0089 is one char, and we also need U+0050 for 'P'
|
||||||
|
# So we need to insert a char
|
||||||
|
|
||||||
|
# The corruption pattern:
|
||||||
|
# - Char 0 (U+5852) replaces two chars: U+0089 and U+0050
|
||||||
|
# - Char 6 (U+000D) should be U+000A
|
||||||
|
# - Char 7 (U+000A) is extra, should be removed
|
||||||
|
# - Char 12 (U+000A) is extra, should be removed
|
||||||
|
|
||||||
|
# Let's verify by looking at what we expect:
|
||||||
|
# Expected chars for correct PNG:
|
||||||
|
# [0]=0x0089, [1]=0x0050('P'), [2]=0x004E('N'), [3]=0x0047('G'),
|
||||||
|
# [4]=0x000D, [5]=0x000A, [6]=0x001A, [7]=0x000A (signature end)
|
||||||
|
# [8]=0x0000, [9]=0x0000, [10]=0x0000, [11]=0x000D (IHDR length=13)
|
||||||
|
# [12]=0x0049('I'), [13]=0x0048('H'), [14]=0x0044('D'), [15]=0x0052('R')
|
||||||
|
|
||||||
|
# Actual chars:
|
||||||
|
# [0]=0x5852, [1]=0x004E, [2]=0x0047, [3]=0x000D, [4]=0x000A,
|
||||||
|
# [5]=0x001A, [6]=0x000D, [7]=0x000A, [8]=0x0000, [9]=0x0000,
|
||||||
|
# [10]=0x0000, [11]=0x000D, [12]=0x000A, [13]=0x0049, [14]=0x0048, ...
|
||||||
|
|
||||||
|
# Fix:
|
||||||
|
# Replace char 0 (U+5852) with U+0089
|
||||||
|
# Insert U+0050 at position 1
|
||||||
|
# Change char 6 from 0x0D to 0x0A
|
||||||
|
# Delete char 7 (extra 0x0A)
|
||||||
|
# Delete char 12 (extra 0x0A) - wait, this is after IHDR length
|
||||||
|
|
||||||
|
fixed_chars = []
|
||||||
|
# Fix char 0: split U+5852 -> U+0089, U+0050
|
||||||
|
fixed_chars.append(0x0089) # PNG first byte
|
||||||
|
fixed_chars.append(0x0050) # 'P'
|
||||||
|
|
||||||
|
# Copy chars 1-5 normally (but adjust indices)
|
||||||
|
# Original char 1 (U+004E='N') -> fixed index 2
|
||||||
|
# Original char 2 (U+0047='G') -> fixed index 3
|
||||||
|
# Original char 3 (U+000D) -> fixed index 4
|
||||||
|
# Original char 4 (U+000A) -> fixed index 5
|
||||||
|
# Original char 5 (U+001A) -> fixed index 6
|
||||||
|
fixed_chars.append(chars[1]) # 'N'
|
||||||
|
fixed_chars.append(chars[2]) # 'G'
|
||||||
|
fixed_chars.append(chars[3]) # '\r'
|
||||||
|
fixed_chars.append(chars[4]) # '\n'
|
||||||
|
fixed_chars.append(chars[5]) # '\x1a'
|
||||||
|
|
||||||
|
# Fix char 6: change from 0x0D to 0x0A
|
||||||
|
fixed_chars.append(0x000A) # Should be '\n' instead of '\r'
|
||||||
|
|
||||||
|
# Skip char 7 (extra 0x0A) - don't add it
|
||||||
|
|
||||||
|
# Add chars 8-11 (IHDR length = 0x0000000D)
|
||||||
|
fixed_chars.append(chars[8]) # 0x0000
|
||||||
|
fixed_chars.append(chars[9]) # 0x0000
|
||||||
|
fixed_chars.append(chars[10]) # 0x0000
|
||||||
|
fixed_chars.append(chars[11]) # 0x000D
|
||||||
|
|
||||||
|
# Skip char 12 (extra 0x0A) - don't add it
|
||||||
|
|
||||||
|
# Add the rest from char 13 onwards
|
||||||
|
for i in range(13, len(chars)):
|
||||||
|
fixed_chars.append(chars[i])
|
||||||
|
|
||||||
|
print(f"Fixed chars: {len(fixed_chars)}")
|
||||||
|
|
||||||
|
# Step 3: Convert fixed chars back to UTF-16 LE bytes
|
||||||
|
fixed_bytes = bytearray()
|
||||||
|
for ch in fixed_chars:
|
||||||
|
fixed_bytes.append(ch & 0xFF) # low byte
|
||||||
|
fixed_bytes.append((ch >> 8) & 0xFF) # high byte
|
||||||
|
|
||||||
|
print(f"Fixed bytes: {len(fixed_bytes)}")
|
||||||
|
|
||||||
|
# Step 4: Extract original PNG bytes (every other byte, starting from first)
|
||||||
|
# In UTF-16 LE, for ASCII chars (< 0x80), the low byte is the char and high byte is 00
|
||||||
|
# But for non-ASCII chars like U+0089, the low byte is 0x89 and high byte is 0x00
|
||||||
|
# So we take the low byte of each char
|
||||||
|
png_bytes = bytearray()
|
||||||
|
for ch in fixed_chars:
|
||||||
|
png_bytes.append(ch & 0xFF)
|
||||||
|
|
||||||
|
print(f"PNG bytes: {len(png_bytes)}")
|
||||||
|
print(f"First 32 PNG bytes: {' '.join(f'{b:02X}' for b in png_bytes[:32])}")
|
||||||
|
|
||||||
|
# Check PNG signature
|
||||||
|
if png_bytes[:8] == b'\x89PNG\r\n\x1a\n':
|
||||||
|
print("✓ Valid PNG signature!")
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'wb') as f:
|
||||||
|
f.write(png_bytes)
|
||||||
|
print("Saved screen_fixed.png")
|
||||||
|
else:
|
||||||
|
print(f"✗ Invalid signature: {' '.join(f'{b:02X}' for b in png_bytes[:8])}")
|
||||||
|
# Try to see if we can find where it goes wrong
|
||||||
|
expected = b'\x89PNG\r\n\x1a\n'
|
||||||
|
for i in range(8):
|
||||||
|
if png_bytes[i] != expected[i]:
|
||||||
|
print(f" Mismatch at byte {i}: got {png_bytes[i]:02X}, expected {expected[i]:02X}")
|
||||||
66
fix_png2.py
Normal file
66
fix_png2.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_fixed.png", 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
print(f"Original size: {len(data)}")
|
||||||
|
|
||||||
|
# The file has been corrupted by text-mode LF->CRLF conversion.
|
||||||
|
# Every 0x0A byte in the original PNG has been changed to 0x0D 0x0A.
|
||||||
|
# We need to revert this: change 0x0D 0x0A back to 0x0A.
|
||||||
|
# But the PNG signature contains a legitimate 0x0D 0x0A (at offset 4-5),
|
||||||
|
# which should remain as-is.
|
||||||
|
|
||||||
|
# Strategy: replace 0D 0A -> 0A, EXCEPT for the known legitimate ones
|
||||||
|
# Legitimate: offset 4-5 (PNG signature "\r\n")
|
||||||
|
|
||||||
|
# Actually, let's just do the replacement everywhere and then fix the signature
|
||||||
|
fixed = bytearray()
|
||||||
|
i = 0
|
||||||
|
while i < len(data):
|
||||||
|
if i < len(data) - 1 and data[i] == 0x0D and data[i+1] == 0x0A:
|
||||||
|
# CR LF found - replace with just LF
|
||||||
|
fixed.append(0x0A)
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
fixed.append(data[i])
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
fixed = bytes(fixed)
|
||||||
|
print(f"After CRLF->LF fix: {len(fixed)}")
|
||||||
|
|
||||||
|
# Now fix the PNG signature (offset 4-5 should be 0D 0A, but we changed it to 0A)
|
||||||
|
# The correct PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
||||||
|
# We need to fix offset 4 to be 0D again
|
||||||
|
if fixed[4] == 0x0A: # Should be 0x0D
|
||||||
|
fixed = fixed[:4] + b'\x0D' + fixed[5:]
|
||||||
|
print("Fixed PNG signature byte 4 (0D)")
|
||||||
|
|
||||||
|
print(f"First 16 bytes: {' '.join(f'{b:02X}' for b in fixed[:16])}")
|
||||||
|
|
||||||
|
# Check IHDR
|
||||||
|
offset = 8
|
||||||
|
length = int.from_bytes(fixed[offset:offset+4], 'big')
|
||||||
|
chunk_type = fixed[offset+4:offset+8].decode('ascii', errors='replace')
|
||||||
|
print(f"IHDR: type={chunk_type}, length={length}")
|
||||||
|
w = int.from_bytes(fixed[offset+8:offset+12], 'big')
|
||||||
|
h = int.from_bytes(fixed[offset+12:offset+16], 'big')
|
||||||
|
bit_depth = fixed[offset+16]
|
||||||
|
color_type = fixed[offset+17]
|
||||||
|
print(f" Width={w}, Height={h}, BitDepth={bit_depth}, ColorType={color_type}")
|
||||||
|
|
||||||
|
# Save
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_fixed2.png", 'wb') as f:
|
||||||
|
f.write(fixed)
|
||||||
|
print("Saved screen_fixed2.png")
|
||||||
|
|
||||||
|
# Try to open with PIL
|
||||||
|
from PIL import Image
|
||||||
|
try:
|
||||||
|
img = Image.open(r"D:\Yuzu-GCA\screen_fixed2.png")
|
||||||
|
print(f"Success! Format={img.format}, Size={img.size}, Mode={img.mode}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"PIL error: {e}")
|
||||||
48
ocr_fixed.py
Normal file
48
ocr_fixed.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
img = Image.open(r"D:\Yuzu-GCA\screen_fixed.png")
|
||||||
|
print(f"Format: {img.format}")
|
||||||
|
print(f"Size: {img.size}")
|
||||||
|
print(f"Mode: {img.mode}")
|
||||||
|
|
||||||
|
# Try OCR
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
|
||||||
|
print("\n=== pytesseract OCR result ===")
|
||||||
|
print(text)
|
||||||
|
except ImportError:
|
||||||
|
print("pytesseract not available")
|
||||||
|
|
||||||
|
# Also try Windows OCR
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
from winsdk.windows.media.ocr import OcrEngine
|
||||||
|
from winsdk.windows.graphics.imaging import BitmapDecoder, SoftwareBitmap
|
||||||
|
from winsdk.windows.storage.streams import RandomAccessStreamReference
|
||||||
|
from winsdk.windows.globalization import Language
|
||||||
|
import os
|
||||||
|
|
||||||
|
async def ocr_windows():
|
||||||
|
engine = OcrEngine.try_create_from_user_profile_languages()
|
||||||
|
if not engine:
|
||||||
|
engine = OcrEngine.try_create_from_language(Language("en"))
|
||||||
|
|
||||||
|
path = os.path.abspath(r"D:\Yuzu-GCA\screen_fixed.png")
|
||||||
|
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
|
||||||
|
decoder = await BitmapDecoder.create_async(file_stream)
|
||||||
|
bitmap = await decoder.get_software_bitmap_async()
|
||||||
|
|
||||||
|
result = await engine.recognize_async(bitmap)
|
||||||
|
print("\n=== Windows OCR result ===")
|
||||||
|
for line in result.lines:
|
||||||
|
print(line.text)
|
||||||
|
|
||||||
|
asyncio.run(ocr_windows())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Windows OCR failed: {e}")
|
||||||
55
ocr_screen.py
Normal file
55
ocr_screen.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
OCR script to extract text from screen.png
|
||||||
|
Attempts to use pytesseract if available, otherwise falls back to basic image info.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
image_path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
# First, try to get basic image info
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
img = Image.open(image_path)
|
||||||
|
print(f"Image size: {img.size}")
|
||||||
|
print(f"Image mode: {img.mode}")
|
||||||
|
print(f"Image format: {img.format}")
|
||||||
|
print("---")
|
||||||
|
except ImportError:
|
||||||
|
print("Pillow not available, trying basic file read...")
|
||||||
|
# Can't do much without PIL
|
||||||
|
|
||||||
|
# Try pytesseract
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
|
||||||
|
print("OCR Result (pytesseract):")
|
||||||
|
print(text)
|
||||||
|
except ImportError:
|
||||||
|
print("pytesseract not available")
|
||||||
|
# Try subprocess call to system tesseract
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['tesseract', image_path, 'stdout', '-l', 'chi_sim+eng'],
|
||||||
|
capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
print("OCR Result (system tesseract):")
|
||||||
|
print(result.stdout)
|
||||||
|
else:
|
||||||
|
print(f"Tesseract error: {result.stderr}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("System tesseract not found either")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error running tesseract: {e}")
|
||||||
|
|
||||||
|
# If nothing works, try reading raw pixels to detect if it's a text screenshot
|
||||||
|
try:
|
||||||
|
from PIL import Image
|
||||||
|
img = Image.open(image_path)
|
||||||
|
# Print first few pixel values to understand the image
|
||||||
|
pixels = list(img.getdata())[:20]
|
||||||
|
print(f"\nFirst 20 pixel values: {pixels}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
23
ocr_screen2.py
Normal file
23
ocr_screen2.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
img = Image.open(r"D:\Yuzu-GCA\screen2.png")
|
||||||
|
print(f"Format: {img.format}, Size: {img.size}, Mode: {img.mode}")
|
||||||
|
|
||||||
|
# Try pytesseract OCR
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
|
||||||
|
# Configure tesseract path if needed
|
||||||
|
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
|
||||||
|
|
||||||
|
text = pytesseract.image_to_string(img, lang='chi_sim+eng')
|
||||||
|
print("\n=== OCR Result ===")
|
||||||
|
print(text)
|
||||||
|
except ImportError:
|
||||||
|
print("\npytesseract not available")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nOCR error: {e}")
|
||||||
43
ocr_script.ps1
Normal file
43
ocr_script.ps1
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# PowerShell script using Windows built-in OCR
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||||
|
Add-Type -AssemblyName Windows.Foundation
|
||||||
|
Add-Type -AssemblyName Windows.Graphics.Imaging
|
||||||
|
Add-Type -AssemblyName Windows.Media.Ocr
|
||||||
|
|
||||||
|
$winRtTypes = [Windows.Media.Ocr.OcrEngine]::GetType()
|
||||||
|
|
||||||
|
# Load the image
|
||||||
|
$imagePath = "D:\Yuzu-GCA\screen2.png"
|
||||||
|
$bitmap = [System.Drawing.Bitmap]::FromFile($imagePath)
|
||||||
|
Write-Host "Image size: $($bitmap.Width) x $($bitmap.Height)"
|
||||||
|
|
||||||
|
# Convert to stream
|
||||||
|
$ms = New-Object System.IO.MemoryStream
|
||||||
|
$bitmap.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$bitmap.Dispose()
|
||||||
|
|
||||||
|
# Create BitmapDecoder
|
||||||
|
$ms.Seek(0, [System.IO.SeekOrigin]::Begin) | Out-Null
|
||||||
|
$randomAccessStream = New-Object Windows.Storage.Streams.InMemoryRandomAccessStream
|
||||||
|
$outputStream = $randomAccessStream.GetOutputStreamAt(0)
|
||||||
|
$dataWriter = New-Object Windows.Storage.Streams.DataWriter($outputStream)
|
||||||
|
$bytes = $ms.ToArray()
|
||||||
|
$dataWriter.WriteBytes($bytes)
|
||||||
|
$dataWriter.StoreAsync().GetAwaiter().GetResult() | Out-Null
|
||||||
|
$dataWriter.FlushAsync().GetAwaiter().GetResult() | Out-Null
|
||||||
|
$ms.Dispose()
|
||||||
|
|
||||||
|
$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($randomAccessStream).GetAwaiter().GetResult()
|
||||||
|
$softwareBitmap = [Windows.Graphics.Imaging.SoftwareBitmap]::Convert($decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult(), [Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8, [Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied)
|
||||||
|
|
||||||
|
# Perform OCR
|
||||||
|
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages()
|
||||||
|
$result = $engine.RecognizeAsync($softwareBitmap).GetAwaiter().GetResult()
|
||||||
|
|
||||||
|
# Output results
|
||||||
|
Write-Host "Recognized text:"
|
||||||
|
Write-Host "===================="
|
||||||
|
foreach ($line in $result.Lines) {
|
||||||
|
Write-Host $line.Text
|
||||||
|
}
|
||||||
51
ocr_script.py
Normal file
51
ocr_script.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
OCR script to extract text from screen.png
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
image_path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
# Try pytesseract first
|
||||||
|
try:
|
||||||
|
import pytesseract
|
||||||
|
from PIL import Image
|
||||||
|
img = Image.open(image_path)
|
||||||
|
text = pytesseract.image_to_string(img, lang='eng+chi_sim')
|
||||||
|
print("=== pytesseract OCR result ===")
|
||||||
|
print(text)
|
||||||
|
sys.exit(0)
|
||||||
|
except ImportError:
|
||||||
|
print("pytesseract not available, trying alternative...")
|
||||||
|
|
||||||
|
# Try Windows OCR via winrt
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
from winsdk.windows.media.ocr import OcrEngine
|
||||||
|
from winsdk.windows.graphics.imaging import BitmapDecoder
|
||||||
|
from winsdk.windows.storage.streams import RandomAccessStreamReference
|
||||||
|
from winsdk.windows.globalization import Language
|
||||||
|
|
||||||
|
async def ocr_windows():
|
||||||
|
# Get the default OCR engine for Chinese
|
||||||
|
engine = OcrEngine.try_create_from_user_profile_languages()
|
||||||
|
if not engine:
|
||||||
|
# Fallback to English
|
||||||
|
engine = OcrEngine.try_create_from_language(Language("en"))
|
||||||
|
|
||||||
|
# Open the image file
|
||||||
|
file_stream = await RandomAccessStreamReference.create_from_file(image_path).open_read_async()
|
||||||
|
decoder = await BitmapDecoder.create_async(file_stream)
|
||||||
|
bitmap = await decoder.get_software_bitmap_async()
|
||||||
|
|
||||||
|
result = await engine.recognize_async(bitmap)
|
||||||
|
print("=== Windows OCR result ===")
|
||||||
|
for line in result.lines:
|
||||||
|
print(line.text)
|
||||||
|
|
||||||
|
asyncio.run(ocr_windows())
|
||||||
|
sys.exit(0)
|
||||||
|
except ImportError:
|
||||||
|
print("winsdk not available either")
|
||||||
|
|
||||||
|
print("No OCR engine available. Please install pytesseract or Windows SDK.")
|
||||||
48
ocr_win.py
Normal file
48
ocr_win.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import sys
|
||||||
|
import io
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||||
|
|
||||||
|
print("Attempting Windows OCR...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
from winsdk.windows.media.ocr import OcrEngine
|
||||||
|
from winsdk.windows.graphics.imaging import BitmapDecoder
|
||||||
|
from winsdk.windows.storage.streams import RandomAccessStreamReference
|
||||||
|
from winsdk.windows.globalization import Language
|
||||||
|
import os
|
||||||
|
|
||||||
|
async def ocr():
|
||||||
|
path = os.path.abspath(r"D:\Yuzu-GCA\screen2.png")
|
||||||
|
print(f"Opening: {path}")
|
||||||
|
|
||||||
|
# Try to get engine for Chinese
|
||||||
|
engine = OcrEngine.try_create_from_user_profile_languages()
|
||||||
|
if not engine:
|
||||||
|
print("No user profile languages, trying Chinese...")
|
||||||
|
engine = OcrEngine.try_create_from_language(Language("zh-Hans"))
|
||||||
|
if not engine:
|
||||||
|
print("Trying English...")
|
||||||
|
engine = OcrEngine.try_create_from_language(Language("en"))
|
||||||
|
|
||||||
|
if not engine:
|
||||||
|
print("No OCR engine available")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Using OCR engine: {engine.recognizer_language.display_name}")
|
||||||
|
|
||||||
|
file_stream = await RandomAccessStreamReference.create_from_file(path).open_read_async()
|
||||||
|
decoder = await BitmapDecoder.create_async(file_stream)
|
||||||
|
bitmap = await decoder.get_software_bitmap_async()
|
||||||
|
|
||||||
|
result = await engine.recognize_async(bitmap)
|
||||||
|
print(f"\n=== Windows OCR Result ({result.lines.size} lines) ===")
|
||||||
|
for line in result.lines:
|
||||||
|
print(line.text)
|
||||||
|
|
||||||
|
asyncio.run(ocr())
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"winsdk not available: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
18
ota/README.md
Normal file
18
ota/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# OTA 文件夹
|
||||||
|
|
||||||
|
此目录存放 manifest 文件。manifest-stable.json 是 CI 构建时动态生成的产物,
|
||||||
|
也提交到仓库通过 raw URL 供客户端拉取。
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
- `gen-manifest.sh` — 构建后运行,计算 APK SHA256 并生成 manifest-stable.json
|
||||||
|
- `manifest-stable.json` — stable 渠道
|
||||||
|
- `manifest-beta.json` — beta 渠道
|
||||||
|
- `manifest-nightly.json` — nightly 渠道
|
||||||
|
|
||||||
|
## 流程
|
||||||
|
|
||||||
|
```
|
||||||
|
CI 构建 APK → ./ota/gen-manifest.sh → git push manifest-stable.json
|
||||||
|
手机 App → raw URL 拉取 manifest → 比较版本 → 下载 APK → SHA256 校验
|
||||||
|
```
|
||||||
12
ota/expo-manifest.json
Normal file
12
ota/expo-manifest.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "e5ce52a-0000-0000-0000-000000000000",
|
||||||
|
"createdAt": "2026-07-19T15:30:00.000Z",
|
||||||
|
"runtimeVersion": "0.1.0",
|
||||||
|
"launchAsset": {
|
||||||
|
"key": "bundle",
|
||||||
|
"contentType": "application/javascript",
|
||||||
|
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/bundle.js"
|
||||||
|
},
|
||||||
|
"assets": [],
|
||||||
|
"metadata": {}
|
||||||
|
}
|
||||||
38
ota/gen-manifest.sh
Normal file
38
ota/gen-manifest.sh
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# OTA manifest 生成脚本
|
||||||
|
# 用法: ./ota/gen-manifest.sh <versionName> <versionCode> <apk-path>
|
||||||
|
# 构建号规则: 每次生成APK时 versionCode +1, 对应 git tag vX.Y.Z-dav-android-N
|
||||||
|
# CI 构建时自动运行, SHA256动态填入, 生成 manifest-stable.json 后上传到 Gitea Release
|
||||||
|
|
||||||
|
VERSION_NAME="${1:-0.1.0}"
|
||||||
|
VERSION_CODE="${2:-7}"
|
||||||
|
APK_PATH="${3:-packages/client-android/android/app/build/outputs/apk/debug/v0.1.0-dav-android-7-debug.apk}"
|
||||||
|
RELEASE_TAG="v${VERSION_NAME}-dav-android-${VERSION_CODE}"
|
||||||
|
APK_NAME="v${VERSION_NAME}-dav-android-${VERSION_CODE}-debug.apk"
|
||||||
|
|
||||||
|
SHA256=$(sha256sum "$APK_PATH" | cut -d' ' -f1)
|
||||||
|
SIZE=$(stat -c%s "$APK_PATH")
|
||||||
|
|
||||||
|
cat > ota/manifest-stable.json <<JSON
|
||||||
|
{
|
||||||
|
"version": "${VERSION_NAME}",
|
||||||
|
"builds": {
|
||||||
|
"android": {
|
||||||
|
"type": "apk",
|
||||||
|
"tag": "${RELEASE_TAG}",
|
||||||
|
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/${RELEASE_TAG}/${APK_NAME}",
|
||||||
|
"sha256": "${SHA256}",
|
||||||
|
"size": ${SIZE},
|
||||||
|
"channel": "stable",
|
||||||
|
"minVersion": "${VERSION_NAME}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
|
||||||
|
echo "Generated: ota/manifest-stable.json"
|
||||||
|
echo " tag: ${RELEASE_TAG}"
|
||||||
|
echo " sha256: ${SHA256}"
|
||||||
|
echo " size: ${SIZE}"
|
||||||
|
echo ""
|
||||||
|
echo "Upload: curl -X POST 'https://git.childish-ghost.com/api/v1/repos/LukeMackin/Yuzu-GCA/releases/\${RELEASE_ID}/assets?name=manifest-stable.json' -H 'Authorization: token \$TOKEN' -H 'Content-Type: application/json' --data-binary @ota/manifest-stable.json"
|
||||||
1
ota/manifest-beta.json
Normal file
1
ota/manifest-beta.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.1.0","builds":{}}
|
||||||
1
ota/manifest-nightly.json
Normal file
1
ota/manifest-nightly.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.1.0","builds":{"android":{"type":"apk","tag":"v0.1.0-dav-android-8","url":"https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-8/v0.1.0-dav-android-8-debug.apk","sha256":"b2451a398ea2dd34e585c5ec4529d6654451c9b9ab4ddf4a25ec9b032f0ac2bf","size":3506569,"channel":"nightly","minVersion":"0.1.0"}}}
|
||||||
1
ota/manifest-stable.json
Normal file
1
ota/manifest-stable.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":"0.1.0","builds":{}}
|
||||||
BIN
packages/client-android/android/.gradle/8.10.2/checksums/checksums.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/checksums.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/checksums/md5-checksums.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/md5-checksums.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/checksums/sha1-checksums.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/checksums/sha1-checksums.bin
vendored
Normal file
Binary file not shown.
0
packages/client-android/android/.gradle/8.10.2/dependencies-accessors/gc.properties
vendored
Normal file
0
packages/client-android/android/.gradle/8.10.2/dependencies-accessors/gc.properties
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/executionHistory/executionHistory.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileChanges/last-build.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileChanges/last-build.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.bin
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.lock
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/fileHashes.lock
vendored
Normal file
Binary file not shown.
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/resourceHashesCache.bin
vendored
Normal file
BIN
packages/client-android/android/.gradle/8.10.2/fileHashes/resourceHashesCache.bin
vendored
Normal file
Binary file not shown.
0
packages/client-android/android/.gradle/8.10.2/gc.properties
vendored
Normal file
0
packages/client-android/android/.gradle/8.10.2/gc.properties
vendored
Normal file
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
#Sun Jul 19 10:50:39 CST 2026
|
||||||
|
gradle.version=8.10.2
|
||||||
Binary file not shown.
BIN
packages/client-android/android/.gradle/file-system.probe
Normal file
BIN
packages/client-android/android/.gradle/file-system.probe
Normal file
Binary file not shown.
40
packages/client-android/android/app/build.gradle
Normal file
40
packages/client-android/android/app/build.gradle
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
apply plugin: "com.android.application"
|
||||||
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "dev.yuzu.gca"
|
||||||
|
compileSdk 35
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "dev.yuzu.gca"
|
||||||
|
minSdk 24
|
||||||
|
targetSdk 35
|
||||||
|
versionCode 8
|
||||||
|
versionName "0.1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release { minifyEnabled false }
|
||||||
|
debug { debuggable true }
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions { jvmTarget = "17" }
|
||||||
|
|
||||||
|
applicationVariants.all { variant ->
|
||||||
|
variant.outputs.all {
|
||||||
|
outputFileName = "v${variant.versionName}-dav-android-${variant.versionCode}-${variant.buildType.name}.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.0.21"
|
||||||
|
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||||
|
implementation "androidx.core:core:1.15.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="Yuzu GCA v0.1.0"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.AppCompat.DayNight">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="Yuzu GCA v0.1.0"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package dev.yuzu.gca
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
class MainActivity : AppCompatActivity() {
|
||||||
|
private lateinit var logView: TextView
|
||||||
|
private var selectedChannel = "stable"
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val scroll = ScrollView(this)
|
||||||
|
val layout = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.VERTICAL
|
||||||
|
setPadding(24, 24, 24, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
val title = TextView(this).apply {
|
||||||
|
text = "Yuzu GCA"
|
||||||
|
textSize = 20f
|
||||||
|
setPadding(0, 0, 0, 4)
|
||||||
|
}
|
||||||
|
layout.addView(title)
|
||||||
|
|
||||||
|
val versionInfo = TextView(this).apply {
|
||||||
|
text = "v0.1.0-dav-android-8-debug"
|
||||||
|
textSize = 12f
|
||||||
|
setTextColor(Color.GRAY)
|
||||||
|
setPadding(0, 0, 0, 12)
|
||||||
|
}
|
||||||
|
layout.addView(versionInfo)
|
||||||
|
|
||||||
|
val channelLabel = TextView(this).apply {
|
||||||
|
text = "选择更新渠道:"
|
||||||
|
textSize = 13f
|
||||||
|
setPadding(0, 8, 0, 4)
|
||||||
|
}
|
||||||
|
layout.addView(channelLabel)
|
||||||
|
|
||||||
|
// 渠道按钮行
|
||||||
|
val channelRow = LinearLayout(this).apply {
|
||||||
|
orientation = LinearLayout.HORIZONTAL
|
||||||
|
setPadding(0, 0, 0, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeChannelBtn(label: String, channel: String): Button {
|
||||||
|
return Button(this).apply {
|
||||||
|
text = label
|
||||||
|
textSize = 12f
|
||||||
|
setOnClickListener {
|
||||||
|
selectedChannel = channel
|
||||||
|
highlightChannel(channelRow, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val btnStable = makeChannelBtn("Stable", "stable")
|
||||||
|
val btnBeta = makeChannelBtn("Beta", "beta")
|
||||||
|
val btnNightly = makeChannelBtn("Nightly", "nightly")
|
||||||
|
|
||||||
|
channelRow.addView(btnStable)
|
||||||
|
channelRow.addView(btnBeta)
|
||||||
|
channelRow.addView(btnNightly)
|
||||||
|
layout.addView(channelRow)
|
||||||
|
highlightChannel(channelRow, btnStable)
|
||||||
|
|
||||||
|
logView = TextView(this).apply {
|
||||||
|
textSize = 12f
|
||||||
|
setTextColor(Color.LTGRAY)
|
||||||
|
}
|
||||||
|
layout.addView(logView)
|
||||||
|
|
||||||
|
val btn = Button(this).apply {
|
||||||
|
text = "检查更新"
|
||||||
|
setOnClickListener {
|
||||||
|
val ch = selectedChannel
|
||||||
|
CoroutineScope(Dispatchers.IO).launch { doOtaTest(ch) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout.addView(btn)
|
||||||
|
|
||||||
|
scroll.addView(layout)
|
||||||
|
setContentView(scroll)
|
||||||
|
|
||||||
|
appendLog("渠道: stable | 点击[检查更新]开始")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun highlightChannel(row: LinearLayout, active: Button) {
|
||||||
|
for (i in 0 until row.childCount) {
|
||||||
|
val b = row.getChildAt(i) as Button
|
||||||
|
b.isSelected = (b == active)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendLog(msg: String) {
|
||||||
|
Log.i(TAG, msg)
|
||||||
|
runOnUiThread { logView.append("$msg\n") }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object { private const val TAG = "GCA-OTA" }
|
||||||
|
|
||||||
|
private fun manifestUrl(channel: String): String {
|
||||||
|
val base = "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota"
|
||||||
|
return when (channel) {
|
||||||
|
"beta" -> "$base/manifest-beta.json"
|
||||||
|
"nightly" -> "$base/manifest-nightly.json"
|
||||||
|
else -> "$base/manifest-stable.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doOtaTest(channel: String) = withContext(Dispatchers.IO) {
|
||||||
|
val url = manifestUrl(channel)
|
||||||
|
appendLog("=== OTA 渠道: $channel ===")
|
||||||
|
appendLog("manifest: $url")
|
||||||
|
|
||||||
|
// 1. 拉取 manifest
|
||||||
|
appendLog("[1/4] 拉取 manifest...")
|
||||||
|
val manifestJson: String
|
||||||
|
try {
|
||||||
|
manifestJson = httpGet(url)
|
||||||
|
appendLog(" ✅ 获取成功 (${manifestJson.length}B)")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appendLog(" ❌ 获取失败: ${e.message}")
|
||||||
|
appendLog("=== 测试中止 ===")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析
|
||||||
|
appendLog("[2/4] 解析 manifest...")
|
||||||
|
var manifestSha256 = ""
|
||||||
|
var manifestApkUrl = ""
|
||||||
|
var manifestTag = ""
|
||||||
|
try {
|
||||||
|
val json = JSONObject(manifestJson)
|
||||||
|
if (!json.getJSONObject("builds").has("android")) {
|
||||||
|
appendLog(" ℹ️ 该渠道暂无可用更新")
|
||||||
|
appendLog("=== $channel 渠道测试完成 ===")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val android = json.getJSONObject("builds").getJSONObject("android")
|
||||||
|
manifestSha256 = android.getString("sha256")
|
||||||
|
manifestApkUrl = android.getString("url")
|
||||||
|
manifestTag = android.getString("tag")
|
||||||
|
appendLog(" 渠道: $channel 版本: $manifestTag")
|
||||||
|
appendLog(" SHA256: ${manifestSha256.take(16)}...")
|
||||||
|
|
||||||
|
// 版本比较:一致则跳过
|
||||||
|
val localTag = "v0.1.0-dav-android-${packageManager.getPackageInfo(packageName, 0).versionCode}"
|
||||||
|
if (manifestTag == localTag) {
|
||||||
|
appendLog(" ✅ 已是最新版本,无需更新")
|
||||||
|
appendLog("=== $channel 渠道测试完成 ===")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
appendLog(" 发现新版本,开始更新...")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appendLog(" ❌ 解析失败: ${e.message}")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 下载
|
||||||
|
appendLog("[3/4] 下载 APK...")
|
||||||
|
val apkFile = File(cacheDir, "update-$channel.apk")
|
||||||
|
try {
|
||||||
|
downloadFile(manifestApkUrl, apkFile)
|
||||||
|
appendLog(" ✅ 下载完成: ${apkFile.length()} bytes")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appendLog(" ❌ 下载失败: ${e.message}")
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. SHA256 校验
|
||||||
|
appendLog("[4/4] SHA256 校验...")
|
||||||
|
val actualSha256 = computeSha256(apkFile)
|
||||||
|
if (actualSha256 == manifestSha256) {
|
||||||
|
appendLog(" ✅ SHA256 校验通过!")
|
||||||
|
// 5. 安装 APK
|
||||||
|
appendLog("[5/5] 提交安装...")
|
||||||
|
try {
|
||||||
|
installApk(apkFile)
|
||||||
|
appendLog(" ✅ 安装已提交,稍后通知栏点击重启")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appendLog(" ❌ 安装失败: ${e.message}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendLog(" ❌ SHA256 校验失败!")
|
||||||
|
appendLog(" expected: ${manifestSha256.take(32)}")
|
||||||
|
appendLog(" actual: ${actualSha256.take(32)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLog("=== $channel 渠道测试完成 ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun httpGet(url: String): String {
|
||||||
|
val conn = URL(url).openConnection() as HttpURLConnection
|
||||||
|
conn.connectTimeout = 10000
|
||||||
|
conn.readTimeout = 10000
|
||||||
|
return conn.inputStream.bufferedReader().readText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ALLOWED_HOSTS = setOf("git.childish-ghost.com", "github.com")
|
||||||
|
|
||||||
|
private fun downloadFile(url: String, dest: File) {
|
||||||
|
val host = URL(url).host
|
||||||
|
if (!ALLOWED_HOSTS.any { host == it || host.endsWith(".$it") }) {
|
||||||
|
throw SecurityException("不允许的下载域名: $host")
|
||||||
|
}
|
||||||
|
val conn = URL(url).openConnection() as HttpURLConnection
|
||||||
|
conn.connectTimeout = 30000
|
||||||
|
conn.readTimeout = 120000
|
||||||
|
conn.inputStream.use { input ->
|
||||||
|
FileOutputStream(dest).use { output -> input.copyTo(output) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun installApk(apkFile: File) {
|
||||||
|
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", apkFile)
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeSha256(file: File): String {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
file.inputStream().use { input ->
|
||||||
|
val buf = ByteArray(8192)
|
||||||
|
var read: Int
|
||||||
|
while (input.read(buf).also { read = it } != -1) {
|
||||||
|
digest.update(buf, 0, read)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="primary">#3b82f6</color>
|
||||||
|
<color name="background">#0f172a</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Yuzu GCA v0.1.0</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<cache-path name="apk" path="/" />
|
||||||
|
</paths>
|
||||||
26
packages/client-android/android/build.gradle
Normal file
26
packages/client-android/android/build.gradle
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Top-level build file
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
buildToolsVersion = "35.0.0"
|
||||||
|
minSdkVersion = 24
|
||||||
|
compileSdkVersion = 35
|
||||||
|
targetSdkVersion = 35
|
||||||
|
kotlinVersion = "2.0.21"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath("com.android.tools.build:gradle:8.7.3")
|
||||||
|
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/client-android/android/gradle.properties
Normal file
5
packages/client-android/android/gradle.properties
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
newArchEnabled=true
|
||||||
|
hermesEnabled=true
|
||||||
BIN
packages/client-android/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
packages/client-android/android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
packages/client-android/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
26
packages/client-android/android/gradlew.bat
vendored
Normal file
26
packages/client-android/android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
set ANDROID_HOME=C:\Users\Middl\AppData\Local\Android\Sdk
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
goto fail
|
||||||
|
:fail
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
exit /b 1
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
goto fail
|
||||||
|
:execute
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
:end
|
||||||
3
packages/client-android/android/settings.gradle
Normal file
3
packages/client-android/android/settings.gradle
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
rootProject.name = 'YuzuGCA'
|
||||||
|
|
||||||
|
include ':app'
|
||||||
@@ -3,40 +3,26 @@
|
|||||||
"name": "YuzuGCA",
|
"name": "YuzuGCA",
|
||||||
"slug": "yuzu-gca",
|
"slug": "yuzu-gca",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"runtimeVersion": "0.1.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"newArchEnabled": true,
|
"newArchEnabled": true,
|
||||||
"splash": {
|
"splash": { "backgroundColor": "#0f172a" },
|
||||||
"backgroundColor": "#0f172a"
|
"ios": { "supportsTablet": false, "bundleIdentifier": "dev.yuzu.gca" },
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"supportsTablet": false,
|
|
||||||
"bundleIdentifier": "dev.yuzu.gca"
|
|
||||||
},
|
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": { "backgroundColor": "#0f172a" },
|
||||||
"backgroundColor": "#0f172a"
|
|
||||||
},
|
|
||||||
"package": "dev.yuzu.gca",
|
"package": "dev.yuzu.gca",
|
||||||
"allowBackup": false,
|
"allowBackup": false,
|
||||||
"permissions": [
|
"versionCode": 8,
|
||||||
"REQUEST_INSTALL_PACKAGES"
|
"permissions": ["REQUEST_INSTALL_PACKAGES"]
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["expo-updates"],
|
||||||
"expo-updates"
|
|
||||||
],
|
|
||||||
"updates": {
|
"updates": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"checkAutomatically": "ON_LOAD",
|
"checkAutomatically": "ON_LOAD",
|
||||||
"fallbackToCacheTimeout": 3000,
|
"fallbackToCacheTimeout": 3000,
|
||||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/manifest/expo-manifest.json"
|
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/raw/branch/main/ota/expo-manifest.json"
|
||||||
},
|
},
|
||||||
"extra": {
|
"extra": { "eas": { "projectId": "yuzu-gca-android" } }
|
||||||
"eas": {
|
|
||||||
"projectId": "yuzu-gca-android"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
packages/client-android/eas.json
Normal file
23
packages/client-android/eas.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 14.0.0"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"preview": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk",
|
||||||
|
"env": {
|
||||||
|
"APP_VERSION": "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"android": {
|
||||||
|
"buildType": "apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.1.0",
|
|
||||||
"builds": {
|
|
||||||
"android": {
|
|
||||||
"type": "apk",
|
|
||||||
"tag": "v0.1.0-dav-android-1",
|
|
||||||
"url": "https://git.childish-ghost.com/LukeMackin/Yuzu-GCA/releases/download/v0.1.0-dav-android-1/gca-0.1.0.apk",
|
|
||||||
"sha256": "PLACEHOLDER",
|
|
||||||
"size": 0,
|
|
||||||
"minVersion": "0.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package dev.yuzu.gca
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import expo.modules.kotlin.modules.Module
|
|
||||||
import expo.modules.kotlin.modules.ModuleDefinition
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.security.DigestInputStream
|
|
||||||
import java.security.MessageDigest
|
|
||||||
|
|
||||||
class PackageInstallerModule : Module() {
|
|
||||||
override fun definition() = ModuleDefinition {
|
|
||||||
Name("PackageInstallerModule")
|
|
||||||
|
|
||||||
AsyncFunction("installApk") { filePath: String, expectedSha256: String ->
|
|
||||||
installApk(filePath, expectedSha256)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun installApk(filePath: String, expectedSha256: String) {
|
|
||||||
val context: Context = appContext.reactContext ?: return
|
|
||||||
val apkFile = File(filePath)
|
|
||||||
if (!apkFile.exists()) throw Exception("安装包不存在")
|
|
||||||
|
|
||||||
if (!context.packageManager.canRequestPackageInstalls()) {
|
|
||||||
throw SecurityException("REQUEST_INSTALL_PACKAGES 权限未授予")
|
|
||||||
}
|
|
||||||
|
|
||||||
val packageInstaller = context.packageManager.packageInstaller
|
|
||||||
val sessionParams = PackageInstaller.SessionParams(
|
|
||||||
PackageInstaller.SessionParams.MODE_FULL_INSTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
val sessionId = packageInstaller.createSession(sessionParams)
|
|
||||||
val session = packageInstaller.openSession(sessionId)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 边写入边计算 SHA256(原子化,避免 TOCTOU)
|
|
||||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
|
||||||
FileInputStream(apkFile).use { input ->
|
|
||||||
val digestStream = DigestInputStream(input, sha256)
|
|
||||||
session.openWrite("package", 0, apkFile.length()).use { output ->
|
|
||||||
digestStream.copyTo(output)
|
|
||||||
session.fsync(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val actualSha256 = sha256.digest().joinToString("") { "%02x".format(it) }
|
|
||||||
if (actualSha256 != expectedSha256) {
|
|
||||||
throw SecurityException("SHA256校验失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
||||||
?: throw Exception("无法获取启动Intent")
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
context, 0, launchIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
session.commit(pendingIntent.intentSender)
|
|
||||||
} finally {
|
|
||||||
session.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"platform": "android",
|
|
||||||
"modules": ["PackageInstallerModule"]
|
|
||||||
}
|
|
||||||
35
read_png.py
Normal file
35
read_png.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Read screen.png as UTF-16 LE text and save decoded text to file.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
out_path = r"D:\Yuzu-GCA\screen_decoded.txt"
|
||||||
|
|
||||||
|
# Read raw bytes
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
print(f"File size: {len(raw)} bytes")
|
||||||
|
|
||||||
|
# Decode as UTF-16 LE (skip BOM)
|
||||||
|
text = raw.decode('utf-16-le')
|
||||||
|
print(f"Decoded text length: {len(text)} characters")
|
||||||
|
|
||||||
|
# Write full decoded text to file
|
||||||
|
with open(out_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(text)
|
||||||
|
print(f"Full decoded text written to: {out_path}")
|
||||||
|
|
||||||
|
# Print first 3000 chars (replace non-printable)
|
||||||
|
safe_text = text[:3000].encode('ascii', errors='replace').decode('ascii')
|
||||||
|
print("\n=== First 3000 characters (non-ASCII replaced) ===")
|
||||||
|
print(safe_text)
|
||||||
|
|
||||||
|
# Search for specific keywords
|
||||||
|
keywords = ['manifest', 'download', 'SHA256', 'verify', 'install', 'OTA', 'version', 'error', 'fail', 'success', 'pass', 'update', '下载', '校验', '安装', '版本']
|
||||||
|
print("\n=== Keyword search ===")
|
||||||
|
for kw in keywords:
|
||||||
|
count = text.count(kw)
|
||||||
|
if count > 0:
|
||||||
|
print(f"'{kw}' found {count} times")
|
||||||
73
recover_png.py
Normal file
73
recover_png.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""
|
||||||
|
Try to recover the original image from screen.png.
|
||||||
|
|
||||||
|
Strategy 1: Decode as UTF-16 LE, then encode back to bytes via UTF-16 LE.
|
||||||
|
This should give us the raw bytes without the BOM.
|
||||||
|
|
||||||
|
Strategy 2: Try to interpret as raw pixel data (e.g. RGBA raw dump).
|
||||||
|
|
||||||
|
Strategy 3: Check if the decoded text contains base64 encoded image data.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
# Decode as UTF-16 LE (with BOM automatically handled by Python)
|
||||||
|
text = raw.decode('utf-16-le')
|
||||||
|
|
||||||
|
# Strategy 1: Re-encode as UTF-16 LE to get bytes
|
||||||
|
reencoded = text.encode('utf-16-le')
|
||||||
|
print(f"Re-encoded size: {len(reencoded)} bytes")
|
||||||
|
|
||||||
|
# Check if it's a valid PNG after removing BOM
|
||||||
|
# Original raw starts with ff fe, so raw[2:] should equal reencoded
|
||||||
|
print(f"Original raw[2:] == reencoded: {raw[2:] == reencoded}")
|
||||||
|
|
||||||
|
# Now, what if the original binary PNG was processed wrong?
|
||||||
|
# Let's look at the first few bytes of reencoded
|
||||||
|
print(f"First 20 bytes of reencoded: {reencoded[:20].hex(' ')}")
|
||||||
|
print(f"First 20 bytes of raw (no BOM): {raw[2:22].hex(' ')}")
|
||||||
|
|
||||||
|
# The reencoded bytes should NOT be a valid PNG (since they start with RXNG etc.)
|
||||||
|
# But maybe the original PNG bytes were: each char's ord() is two PNG bytes?
|
||||||
|
# For ASCII chars like 'I', ord('I')=0x49, but 'I' in UTF-16LE is 49 00
|
||||||
|
# So each char gives us: low_byte = ord(c) & 0xFF, high_byte = (ord(c) >> 8) & 0xFF
|
||||||
|
# If the original PNG had 49 00 at some point, that would become 'I' in UTF-16LE text
|
||||||
|
|
||||||
|
# Let's try: take each char, extract its two bytes
|
||||||
|
# For chars in BMP: char -> code point -> 2 bytes
|
||||||
|
bytes_from_chars = bytearray()
|
||||||
|
for ch in text:
|
||||||
|
cp = ord(ch)
|
||||||
|
if cp <= 0xFFFF:
|
||||||
|
bytes_from_chars.append(cp & 0xFF)
|
||||||
|
bytes_from_chars.append((cp >> 8) & 0xFF)
|
||||||
|
else:
|
||||||
|
# Surrogate pair - skip for now
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"\nBytes from chars (first 20): {bytes(bytes_from_chars[:20]).hex(' ')}")
|
||||||
|
|
||||||
|
# Check if this is a valid PNG
|
||||||
|
if bytes_from_chars[:8] == b'\x89PNG\r\n\x1a\n':
|
||||||
|
print("SUCCESS: This is a valid PNG!")
|
||||||
|
# Write the recovered PNG
|
||||||
|
with open(r"D:\Yuzu-GCA\screen_recovered.png", 'wb') as f:
|
||||||
|
f.write(bytes_from_chars)
|
||||||
|
print("Recovered PNG written to screen_recovered.png")
|
||||||
|
else:
|
||||||
|
print(f"Not a PNG. First 8 bytes: {bytes(bytes_from_chars[:8]).hex(' ')}")
|
||||||
|
# Check what it starts with
|
||||||
|
print(f"As text: {bytes(bytes_from_chars[:8])}")
|
||||||
|
|
||||||
|
# Strategy 2: Maybe the text contains base64?
|
||||||
|
# Search for base64 patterns
|
||||||
|
b64_pattern = r'[A-Za-z0-9+/=]{50,}'
|
||||||
|
b64_matches = re.findall(b64_pattern, text)
|
||||||
|
print(f"\nBase64-like strings found: {len(b64_matches)}")
|
||||||
|
for m in b64_matches[:3]:
|
||||||
|
print(f" {m[:80]}...")
|
||||||
BIN
screen2.png
Normal file
BIN
screen2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
BIN
screen_decoded.txt
Normal file
BIN
screen_decoded.txt
Normal file
Binary file not shown.
BIN
screen_fixed.png
Normal file
BIN
screen_fixed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
screen_fixed2.png
Normal file
BIN
screen_fixed2.png
Normal file
Binary file not shown.
BIN
screen_full_text.txt
Normal file
BIN
screen_full_text.txt
Normal file
Binary file not shown.
35
search_text.py
Normal file
35
search_text.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"""
|
||||||
|
Search for readable ASCII strings in the decoded text from screen.png.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
path = r"D:\Yuzu-GCA\screen.png"
|
||||||
|
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
|
||||||
|
# Decode as UTF-16 LE
|
||||||
|
text = raw.decode('utf-16-le')
|
||||||
|
|
||||||
|
# Find all printable ASCII sequences of length >= 4
|
||||||
|
matches = re.findall(r'[ -~]{4,}', text)
|
||||||
|
print(f"Found {len(matches)} ASCII strings of length >= 4")
|
||||||
|
print("\n=== First 60 matches ===")
|
||||||
|
for i, m in enumerate(matches[:60]):
|
||||||
|
print(f" [{i}] '{m}'")
|
||||||
|
|
||||||
|
# Also search for specific patterns
|
||||||
|
patterns = ['OTA', 'update', 'download', 'install', 'version', 'error', 'fail',
|
||||||
|
'success', 'manifest', 'SHA256', 'verify', 'check', '完成', '下载',
|
||||||
|
'安装', '校验', '版本', '测试', '日志']
|
||||||
|
print("\n=== Pattern search ===")
|
||||||
|
for p in patterns:
|
||||||
|
count = text.count(p)
|
||||||
|
if count > 0:
|
||||||
|
# Find context around first occurrence
|
||||||
|
idx = text.find(p)
|
||||||
|
start = max(0, idx - 30)
|
||||||
|
end = min(len(text), idx + len(p) + 50)
|
||||||
|
ctx = text[start:end].encode('ascii', errors='replace').decode('ascii')
|
||||||
|
print(f" '{p}' found {count} times. First context: ...{ctx}...")
|
||||||
1
tessdata_temp
Submodule
1
tessdata_temp
Submodule
Submodule tessdata_temp added at ced78752cc
Reference in New Issue
Block a user