Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1344589d8d | ||
|
|
049e3c3387 | ||
|
|
68b3f2e543 | ||
|
|
2c16fcea9d | ||
|
|
8638b8d824 | ||
|
|
e259a5e436 | ||
|
|
2bd26b6138 | ||
|
|
18ee4f78fe | ||
|
|
57dfadd3d5 | ||
|
|
6ef2ef718b | ||
|
|
2fc7b060ff | ||
|
|
02c40402bb | ||
|
|
7136819979 | ||
|
|
c9af436951 | ||
|
|
2d928222c6 | ||
|
|
90f2b47ca5 | ||
|
|
06c37f0b63 | ||
|
|
77191e1885 | ||
|
|
7510fba1c1 | ||
|
|
6450b419ad | ||
|
|
70e953d499 | ||
|
|
9d55ec15a6 | ||
|
|
4338e34694 | ||
|
|
433578f4bf | ||
|
|
32312072ab | ||
|
|
0d8136bed5 | ||
|
|
e84fc52eeb | ||
|
|
2990f36483 | ||
|
|
7763aecfa3 | ||
|
|
8db2d6d11a | ||
|
|
1d5ef17499 | ||
|
|
c7a1923403 | ||
|
|
5dfc5de86e | ||
|
|
630c15fed2 | ||
|
|
3ee1d56926 | ||
|
|
4978d20847 | ||
|
|
0b52a123c4 | ||
|
|
5c5ea71a62 | ||
|
|
9b24348465 | ||
|
|
5723e2a078 | ||
|
|
e9189ceced | ||
|
|
33bff2c11e | ||
|
|
d5531c2658 | ||
|
|
020c550b21 | ||
|
|
70e60e5b9d | ||
|
|
43cda58e71 | ||
|
|
a55f8ec341 | ||
|
|
fff040279c | ||
|
|
8810f35c22 | ||
|
|
476997032b | ||
|
|
c2148e17bf | ||
|
|
593a2040a0 | ||
|
|
45566b28b6 | ||
|
|
1c1325911a | ||
|
|
39587c9c3b | ||
|
|
460a0b82d7 | ||
|
|
38031f3317 | ||
|
|
3cdcbeb020 | ||
|
|
bad354e6dd | ||
|
|
538a2e0f66 | ||
|
|
5d0acef6db | ||
|
|
af5db3cc6c | ||
|
|
208deee0e3 | ||
|
|
ef2ea15d9b | ||
|
|
35f2a2956a | ||
|
|
6ac9bdb1cc | ||
|
|
e88df3496a | ||
|
|
e63e4de3e5 | ||
|
|
016462d4a9 | ||
|
|
4a5cefbcd1 | ||
|
|
b80bbb39c2 | ||
|
|
f8c2f2bee4 | ||
|
|
24b1ad56e8 | ||
|
|
3e671e16a3 | ||
|
|
d556050327 | ||
|
|
e5cdf99d19 | ||
|
|
31322be438 | ||
|
|
0d8cbb2762 | ||
|
|
8f12344e50 | ||
|
|
15a6eb79bf | ||
|
|
6c60b3d717 | ||
|
|
929bd0da89 | ||
|
|
bb2a924e1f | ||
|
|
fe1113d7c2 | ||
|
|
27a43b653d | ||
|
|
6e94b8c9ad | ||
|
|
6ea8e2e377 | ||
|
|
9793975465 | ||
|
|
45669a38b5 | ||
|
|
59036d3a30 | ||
|
|
93c1eecc23 | ||
|
|
fbd11cce20 | ||
|
|
17d53f4d95 | ||
|
|
838dbd1a85 | ||
|
|
2debbab21c | ||
|
|
eb22611b78 | ||
|
|
98bec94d48 | ||
|
|
9ee58f1ee1 | ||
|
|
3993b8e8b5 | ||
|
|
5fb536dbbe | ||
|
|
7ee5312783 | ||
|
|
d4772c679d | ||
|
|
587e8040f6 | ||
|
|
54fef961b0 | ||
|
|
1cddbadf8c | ||
|
|
b325ab24a9 | ||
|
|
2ad3e4fddc | ||
|
|
8d8ebf925c | ||
|
|
16a2dd769b | ||
|
|
7952910152 | ||
|
|
e0eaa15e3c | ||
|
|
d90ad5b7cd | ||
|
|
2b6f4c6d28 | ||
|
|
0bb1104d98 | ||
|
|
3ed3dbef88 | ||
|
|
d9a9fa8e25 | ||
|
|
9f5e01b990 | ||
|
|
d97a18b4f2 | ||
|
|
0a6ba53d0f | ||
|
|
927c53876f | ||
|
|
562abef35e | ||
|
|
7e23b45294 | ||
|
|
db3434d539 | ||
|
|
f7b66958e8 | ||
|
|
93448f44f9 | ||
|
|
7d463de45e | ||
|
|
7b5daa5b58 | ||
|
|
7710efd19d | ||
|
|
41982c3f2d | ||
|
|
1a5ae2b1f2 | ||
|
|
337c1f8acf | ||
|
|
e970fa2fb5 | ||
|
|
acbd81e5fa | ||
|
|
4ad694e6bc | ||
|
|
c18010356d | ||
|
|
42d6619b06 | ||
|
|
6f9af98ee6 | ||
|
|
a3b641df2e | ||
|
|
45379cb8b5 | ||
|
|
bf7697afaf | ||
|
|
f136829a2b | ||
|
|
618e12ec00 | ||
|
|
a86e63cb7f | ||
|
|
bcc5925a55 | ||
|
|
9d65b23737 | ||
|
|
e73192fd87 | ||
|
|
6874885881 | ||
|
|
7284d72d2b | ||
|
|
9a509ab2d8 | ||
|
|
d3c20d3490 | ||
|
|
d819eca642 | ||
|
|
199ced0087 | ||
|
|
55a33c1cf6 | ||
|
|
079832fc78 | ||
|
|
6742b27352 | ||
|
|
0736cdb1c8 | ||
|
|
a77147a3d3 |
@@ -3,9 +3,3 @@
|
|||||||
output/
|
output/
|
||||||
resource
|
resource
|
||||||
*.db
|
*.db
|
||||||
*.db.decoded
|
|
||||||
*2015*
|
|
||||||
*2017*
|
|
||||||
2020*
|
|
||||||
avatar.index
|
|
||||||
emoji.cache
|
|
||||||
|
|||||||
@@ -1,97 +1,118 @@
|
|||||||
## Render WeChat Messages from Android
|
## Dump WeChat Messages from Android
|
||||||
|
|
||||||
## 安卓微信消息记录分析工具
|
## 导出安卓微信聊天数据
|
||||||
|
|
||||||
WeChat, as the most popular mobile IM app in China, doesn't provide any methods to read its message history.
|
WeChat(微信), as the most popular mobile IM app in China, doesn't give users any method to export well-formatted history message.
|
||||||
Customers are not able to analyze their own chat messages or interact with other data analysis tools.
|
This tool can parse and export WeChat messages on a rooted android phone.
|
||||||
|
|
||||||
We provide this tool that can parse WeChat messages from a rooted android phone.
|
Right now it can dump messages in text-only mode, or generate a single-file html containing voice messages, images, emoji, etc.
|
||||||
This is necessary to provide interoperability between WeChat messages and other message analysis tools.
|
|
||||||
As examples, we provide sample scripts to obtain statistics of the message history and
|
|
||||||
render the messages into self-contained html files including voice messages, images, emojis, videos, etc.
|
|
||||||
Users can also write custom programs based on this tool to manage their chat messages.
|
|
||||||
|
|
||||||
The tool is last verified to work with latest version of WeChat on 2025/01/01.
|
__NEWS__: WeChat 6.0+ uses silk to encode audio. The code is updated.
|
||||||
If the tool works for you, please take a moment to add your phone/OS to [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
|
|
||||||
|
|
||||||
## How to use:
|
__NEWS__: WeChat 6.3 uses a new avatar storage. The code is updated.
|
||||||
|
|
||||||
|
If this tools works for you, please take a moment to __add your phone/OS to__ [the wiki](https://github.com/ppwwyyxx/wechat-dump/wiki).
|
||||||
|
If it doesn't work, please leave an issue together with your phone/OS/wechat version.
|
||||||
|
|
||||||
|
### How to use:
|
||||||
|
|
||||||
#### Dependencies:
|
#### Dependencies:
|
||||||
+ adb and rooted android phone connected to a Linux/Mac OSX/Win10+Bash.
|
+ python-PIL
|
||||||
+ Python >= 3.8
|
+ [PyQuery](https://pypi.python.org/pypi/pyquery/1.2.1)
|
||||||
+ sox (command line tools)
|
+ [pysox](https://pypi.python.org/pypi/pysox/0.3.6.alpha)
|
||||||
+ Silk audio decoder (included; build it with `./third-party/compile_silk.sh`)
|
+ [pysqlcipher](https://pypi.python.org/pypi/pysqlcipher)
|
||||||
+ Other python dependencies: `pip install -r requirements.txt`.
|
+ numpy
|
||||||
|
+ csscompressor (suggested, optional)
|
||||||
|
+ adb and rooted android phone connected to a Linux/Mac OS.
|
||||||
|
+ Silk audio decoder (included; just run `./third-party/compile_silk.sh`)
|
||||||
|
+ gnu-sed
|
||||||
|
|
||||||
#### Get Necessary Data:
|
#### Get Necessary Data:
|
||||||
|
|
||||||
1. Pull database file and (for older WeChat versions) avatar index:
|
Note that commands involving `./android-interact.sh` are meant to be run on the computer.
|
||||||
+ Automatic: `./android-interact.sh db`. It may use an incorrect userid.
|
|
||||||
+ Manual:
|
+ (Requires Linux or Mac) Get the decrypted WeChat database and the avatar index:
|
||||||
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device.
|
+ Automatic: `./android-interact.sh db-decrypt`
|
||||||
It should be a 32-character-long name consisting of hexadecimal digits.
|
+ Requires rooted adb. If the OS distribution does not come with adb support, you can download an app such as https://play.google.com/store/apps/details?id=eu.chainfire.adbd
|
||||||
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/EnMicroMsg.db` from the device.
|
+ Manual:
|
||||||
2. Decode `EnMicroMsg.db`. We do not provide instructions to do that.
|
+ Figure out your `${userid}` by inspecting the contents of `/data/data/com.tencent.mm/MicroMsg` on the __root__ filesystem of the device. It should be a 32-character-long name consisting of hexadecimal digits.
|
||||||
3. Copy the unencrypted WeChat user resource directory `/data/data/com.tencent.mm/MicroMsg/${userid}/{avatar,emoji,image2,sfs,video,voice2}` from the phone to the `resource` directory:
|
+ Get `/data/data/com.tencent.mm/MicroMsg/${userid}/{EnMicroMsg.db,sfs/avatar.index}` from the device, possible ways are:
|
||||||
|
+ `./android-interact.sh db`
|
||||||
|
+ Use your rooted file system manager app
|
||||||
|
+ Get WeChat uin (an integer), possible ways are:
|
||||||
|
+ `./android-interact.sh uin`, which pulls the value from `/data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml`
|
||||||
|
+ Login to [web wechat](https://wx.qq.com), get wxuin=1234567 from `document.cookie`
|
||||||
|
+ Get your phone IMEI number (a positive integer), possible ways are:
|
||||||
|
+ `./android-interact.sh imei`
|
||||||
|
+ Call `*#06#` on your phone
|
||||||
|
+ Find IMEI in system settings
|
||||||
|
+ Decrypt database, will produce `decrypted.db`:
|
||||||
|
|
||||||
|
```
|
||||||
|
./decrypt-db.py <path to EnMicroMsg.db> <imei> <uin>
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: you may need to try different ways to getting imei & uin,
|
||||||
|
because things behave differently on different phones.
|
||||||
|
|
||||||
|
Also, if the decryption doesn't work with pysqlcipher, maybe try the version of sqlcipher in `legacy`.
|
||||||
|
|
||||||
|
|
||||||
|
+ Copy the WeChat user resource directory `/mnt/sdcard/tencent/MicroMsg/${userid}/{emoji,image2,sfs,video,voice2}` from the phone's SD card to the `resource` directory:
|
||||||
+ `./android-interact.sh res`
|
+ `./android-interact.sh res`
|
||||||
+ Change `RES_DIR` in the script if the location of these directories is different on your phone.
|
+ You might need to tweak `RES_DIR` in the script if the default doesn't work
|
||||||
For older version of WeChat, the directory may be `/mnt/sdcard/tencent/MicroMsg/`
|
+ This can take a __long__ time. Some ways to do this faster:
|
||||||
+ This can take a while. It can be faster to first archive it with `tar` with or without compression, and then copy the archive,
|
+ If there's enough free space on the SD card, you can combine all the files via `busybox tar` without compression in the `adb shell`, use `adb pull` to copy the `tar` archive to the computer, and then extract it. BusyBox is needed as the Android system's `tar` may choke on long paths.
|
||||||
`busybox tar` is recommended as the Android system's `tar` may choke on long paths.
|
+ Alternatively, you can use pipes. This is slower, but doesn't require any free space on the SD card:
|
||||||
+ In the end, we need a `resource` directory with the following subdir: `avatar,emoji,image2,sfs,video,voice2`.
|
|
||||||
|
|
||||||
4. (Optional) Install and start a WXGF decoder server on an android device. Without this, certain WXGF images will not be rendered or will be rendered in low resolution.
|
```sh
|
||||||
See [WXGFDecoder](WXGFDecoder) for instructions.
|
# copy MicroMsg to the current directory
|
||||||
|
adb shell 'cd /mnt/sdcard/tencent &&
|
||||||
4. (Optional) Download the emoji cache from [here](https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2)
|
busybox tar czf - MicroMsg 2>/dev/null | busybox base64' |
|
||||||
and decompress it under `wechat-dump`. This will avoid downloading too many emojis during rendering.
|
base64 -di | tar xzf -
|
||||||
|
```
|
||||||
wget -c https://github.com/ppwwyyxx/wechat-dump/releases/download/0.1/emoji.cache.tar.bz2
|
|
||||||
tar xf emoji.cache.tar.bz2
|
|
||||||
|
|
||||||
#### Run:
|
#### Run:
|
||||||
+ Parse and dump text messages of __every__ chat (requires decoded database):
|
+ Parse and dump text messages of __every__ chat (requires `decrypted.db`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./dump-msg.py decoded.db output_dir
|
./dump-msg.py decrypted.db output_dir
|
||||||
```
|
```
|
||||||
|
|
||||||
+ List all chats (required decoded database):
|
+ List all chats (requires `decrypted.db`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./list-chats.py decoded.db
|
./list-chats.py decrypted.db
|
||||||
```
|
```
|
||||||
|
|
||||||
+ Generate statistics report on text messages (requires `output_dir` from `./dump-msg.py`):
|
+ Generate statistical report on text messages (requires `output_dir` from `./dump-msg.py`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./count-message.sh output_dir
|
./count-message.sh output_dir
|
||||||
```
|
```
|
||||||
|
|
||||||
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires decoded database and `resource`):
|
+ Dump messages of one contact to html, containing voice messages, emojis, and images (requires `decrypted.db`, `avatar.index`, and `resource`):
|
||||||
|
|
||||||
```
|
```
|
||||||
./dump-html.py "<contact_display_name>"
|
./dump-html.py decrypted.db avatar.index resource "<contact_name>" output.html
|
||||||
```
|
```
|
||||||
|
|
||||||
* The output file is `output.html`. Check `./dump-html.py -h` to use different input/output paths.
|
|
||||||
* Add `--wxgf-server ws://xx.xx.xx.xx:xxxx` to use a WXGF decoder server.
|
|
||||||
|
|
||||||
### Examples:
|
### Examples:
|
||||||
|
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
||||||
|
|
||||||
Screenshots of generated html:
|
Screenshots of generated html:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
See [here](http://ppwwyyxx.com/static/wechat/example.html) for an example html.
|
### TODO List
|
||||||
|
+ Search by uid/username
|
||||||
### TODO List (help needed!)
|
+ Faster way to copy a directory from android (I don't know..).
|
||||||
* After chat history migration, some emojis in the `EmojiInfo` table don't have corresponding URLs but only a md5 -
|
+ Fix rare unhandled types: > 10000 and < 0
|
||||||
they are not downloaded by WeChat until the message needs to be displayed. We don't know how to manually download these emojis.
|
+ Better user experiences... see `grep 'TODO' wechat -R`
|
||||||
* Decoding WXGF images using an android app is too complex. Looking for an easier way (e.g. qemu).
|
+ more easy-to-use for non-programmers (GUI?)
|
||||||
* Fix rare unhandled message types: > 10000 and < 0
|
|
||||||
* Better user experiences... see `grep 'TODO' wechat -R`
|
|
||||||
|
|
||||||
### Donate!
|
### Donate!
|
||||||
|
Paypal:
|
||||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted">
|
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=7BC299GRDLEDU&lc=US&item_name=wechat%2ddump&item_number=wechat%2ddump¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted">
|
||||||
<img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" />
|
<img src="https://img.shields.io/badge/Paypal-Buy%20a%20Drink-blue.svg" alt="[paypal]" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
*.iml
|
|
||||||
.gradle
|
|
||||||
local.properties
|
|
||||||
.idea
|
|
||||||
app/release
|
|
||||||
.DS_Store
|
|
||||||
build
|
|
||||||
captures
|
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
local.properties
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
|
|
||||||
# WXGF Decoder
|
|
||||||
|
|
||||||
This is an android app that decodes wechat's proprietary "WXGF/WXAM" image format.
|
|
||||||
|
|
||||||
|
|
||||||
## Build & Install
|
|
||||||
|
|
||||||
Open this project directory using android studio, and build against an ARMv8 device.
|
|
||||||
|
|
||||||
Pre-built APKs can be downloaded at [releases](https://github.com/ppwwyyxx/wechat-dump/releases).
|
|
||||||
|
|
||||||
Install the APK to your android phone.
|
|
||||||
|
|
||||||
Note that this app uses native libs, therefore requires an ARMv8 compatible device to run.
|
|
||||||
The native libs are extracted from the WeChat v7.0.22 APK downloaded from [here](https://www.apkmirror.com/apk/wechat-tencent/wechat/wechat-7-0-22-release/wechat-7-0-22-android-apk-download/)
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="screenshot.jpg" alt="screenshot" width="400">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
1. (Optional) Configure the port number if needed, or leave it to default.
|
|
||||||
2. Click "Start Server"
|
|
||||||
3. Use the server address `ws://xx.xx.xx.xx:xxxx` to decode
|
|
||||||
* Make sure this address is accessible from the computer that runs `wechat-dump`.
|
|
||||||
For example, connect them into the same Wifi.
|
|
||||||
4. Once a client is connected and decoding request is received, logs will be printed in the app.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
build
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import com.android.build.api.dsl.Packaging
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.android.application)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "com.ppwwyyxx.wxgfdecoder"
|
|
||||||
compileSdk = 35
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId = "com.ppwwyyxx.wxgfdecoder"
|
|
||||||
minSdk = 24
|
|
||||||
targetSdk = 35
|
|
||||||
versionCode = 1
|
|
||||||
versionName = "1.0"
|
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
|
||||||
}
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding = true
|
|
||||||
}
|
|
||||||
packagingOptions {
|
|
||||||
pickFirst( "jniLibs/arm64-v8a/libvoipCodec.so")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("org.java-websocket:Java-WebSocket:1.5.4")
|
|
||||||
implementation(libs.appcompat)
|
|
||||||
implementation(libs.material)
|
|
||||||
implementation(libs.constraintlayout)
|
|
||||||
implementation(libs.lifecycle.livedata.ktx)
|
|
||||||
implementation(libs.lifecycle.viewmodel.ktx)
|
|
||||||
implementation(libs.navigation.fragment)
|
|
||||||
implementation(libs.navigation.ui)
|
|
||||||
testImplementation(libs.junit)
|
|
||||||
androidTestImplementation(libs.ext.junit)
|
|
||||||
androidTestImplementation(libs.espresso.core)
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# You can control the set of applied configuration files using the
|
|
||||||
# proguardFiles setting in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:extractNativeLibs="true"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.WXAMConverter"
|
|
||||||
tools:targetApi="31">
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/app_name">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.PowerManager;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
import com.tencent.mm.plugin.gif.MMWXGFJNI;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import com.ppwwyyxx.wxgfdecoder.databinding.ActivityMainBinding;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
import org.java_websocket.WebSocket;
|
|
||||||
import org.java_websocket.handshake.ClientHandshake;
|
|
||||||
import org.java_websocket.server.WebSocketServer;
|
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
|
||||||
|
|
||||||
private ActivityMainBinding binding;
|
|
||||||
private Button startButton;
|
|
||||||
private TextView hostnameTextView, portTextView, logTextView;
|
|
||||||
private EditText portEditText;
|
|
||||||
private WebSocketServer server;
|
|
||||||
|
|
||||||
static {
|
|
||||||
System.loadLibrary("wechatcommon");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(binding.getRoot());
|
|
||||||
|
|
||||||
// Keep screen on
|
|
||||||
getWindow().addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
// If the app is running on Android 8.0 or higher, request for ignoring battery optimizations
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
|
||||||
if (!powerManager.isIgnoringBatteryOptimizations(getPackageName())) {
|
|
||||||
// TODO: request for ignoring battery optimizations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MMWXGFJNI.initialize(getApplicationInfo().nativeLibraryDir);
|
|
||||||
|
|
||||||
// Set up UI
|
|
||||||
startButton = findViewById(R.id.start_server_button);
|
|
||||||
hostnameTextView = findViewById(R.id.text_view_hostname);
|
|
||||||
portTextView = findViewById(R.id.text_view_port);
|
|
||||||
portEditText = findViewById(R.id.edit_text_port);
|
|
||||||
logTextView = findViewById(R.id.log_text_area);
|
|
||||||
logTextView.setText("");
|
|
||||||
startButton.setOnClickListener(v -> {
|
|
||||||
try {
|
|
||||||
startWebSocketServer();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Run a unit test:
|
|
||||||
testWxgfDecoder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startWebSocketServer() throws InterruptedException {
|
|
||||||
if (server != null) {
|
|
||||||
server.stop();
|
|
||||||
server = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int port;
|
|
||||||
try {
|
|
||||||
port = Integer.parseInt(portEditText.getText().toString());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
port = 8080;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
server = new WebSocketServer(new InetSocketAddress(port)) {
|
|
||||||
@Override
|
|
||||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
|
||||||
printLog("new connection from " + conn.getRemoteSocketAddress().getAddress().getHostAddress());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
|
||||||
printLog("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onMessage( WebSocket conn, ByteBuffer message ) {
|
|
||||||
byte[] msg = message.array();
|
|
||||||
|
|
||||||
printLog("received binary message from " + conn.getRemoteSocketAddress() + ": size=" + msg.length
|
|
||||||
+ " -- " + Arrays.toString(Arrays.copyOfRange(msg, 0, 10)));
|
|
||||||
|
|
||||||
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(msg);
|
|
||||||
if (res != null) {
|
|
||||||
printLog("After decoding: size=" + res.length + " -- " +
|
|
||||||
Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
|
|
||||||
ByteBuffer response = ByteBuffer.wrap(res);
|
|
||||||
conn.send(response); // the response gif
|
|
||||||
} else {
|
|
||||||
printLog("Failed to decode.");
|
|
||||||
conn.send("FAILED".getBytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage( WebSocket conn, String message) {
|
|
||||||
printLog("Unexpected! received text message from " + conn.getRemoteSocketAddress() + ": " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(WebSocket conn, Exception ex) {
|
|
||||||
printLog("an error occurred on connection " + conn + ":" + ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
try {
|
|
||||||
String hostName = getLocalIpAddress(true);
|
|
||||||
int port = server.getPort();
|
|
||||||
printLog("server started successfully at ws://" + hostName + ":" + port);
|
|
||||||
|
|
||||||
runOnUiThread(() -> { // update UI in UI thread
|
|
||||||
hostnameTextView.setText("Hostname: " + hostName);
|
|
||||||
portTextView.setText("Port: " + String.valueOf(port));
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
server.start();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printLog(String log) {
|
|
||||||
System.out.println(log);
|
|
||||||
runOnUiThread(() -> {
|
|
||||||
String currentLog = logTextView.getText().toString();
|
|
||||||
logTextView.setText(currentLog + "\n" + log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocalIpAddress(boolean useIPv4) throws SocketException {
|
|
||||||
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
|
|
||||||
for (NetworkInterface intf : interfaces) {
|
|
||||||
List<InetAddress> addrs = Collections.list(intf.getInetAddresses());
|
|
||||||
for (InetAddress addr : addrs) {
|
|
||||||
if (!addr.isLoopbackAddress()) {
|
|
||||||
String sAddr = addr.getHostAddress();
|
|
||||||
boolean isIPv4 = sAddr.indexOf(':')<0;
|
|
||||||
if (useIPv4) {
|
|
||||||
if (isIPv4)
|
|
||||||
return sAddr;
|
|
||||||
} else {
|
|
||||||
return sAddr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} }
|
|
||||||
return "localhost";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Methods below are for debugging the decoder:
|
|
||||||
private void testWxgfDecoder() {
|
|
||||||
byte[] wxgf = null;
|
|
||||||
try {
|
|
||||||
wxgf = readBinary(this);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
System.out.println("Unit test: wxgf size=" + wxgf.length);
|
|
||||||
byte[] res = com.tencent.mm.plugin.gif.MMWXGFJNI.nativeWxam2PicBuf(wxgf);
|
|
||||||
System.out.println("Unit test: res size=" + res.length + " -- " + Arrays.toString(Arrays.copyOfRange(res, 0, 10)));
|
|
||||||
|
|
||||||
try {
|
|
||||||
writeFile(this, "test.jpg", res);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
if (wxgf.length != 64367 || res.length != 705188) {
|
|
||||||
throw new RuntimeException("test failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static byte[] readBinary(Context context) throws IOException {
|
|
||||||
InputStream inputStream = context.getResources().openRawResource(R.raw.test_wxgf); // R.raw.image refers to your image.jpg file
|
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
byte[] buffer = new byte[1024];
|
|
||||||
int len;
|
|
||||||
while ((len = inputStream.read(buffer)) != -1) {
|
|
||||||
byteArrayOutputStream.write(buffer, 0, len);
|
|
||||||
}
|
|
||||||
inputStream.close(); // Close the input stream
|
|
||||||
return byteArrayOutputStream.toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeFile(Context context, String filename, byte[] data) throws IOException {
|
|
||||||
File file = new File(context.getExternalFilesDir(null), filename); // Use null for the type argument to get the root external files directory
|
|
||||||
System.out.println("Writing file to: " + file.getAbsolutePath());
|
|
||||||
FileOutputStream outputStream = new FileOutputStream(file);
|
|
||||||
outputStream.write(data);
|
|
||||||
outputStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentDashboardBinding;
|
|
||||||
|
|
||||||
public class DashboardFragment extends Fragment {
|
|
||||||
|
|
||||||
private FragmentDashboardBinding binding;
|
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
DashboardViewModel dashboardViewModel =
|
|
||||||
new ViewModelProvider(this).get(DashboardViewModel.class);
|
|
||||||
|
|
||||||
binding = FragmentDashboardBinding.inflate(inflater, container, false);
|
|
||||||
View root = binding.getRoot();
|
|
||||||
|
|
||||||
final TextView textView = binding.textDashboard;
|
|
||||||
dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.dashboard;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
|
|
||||||
public class DashboardViewModel extends ViewModel {
|
|
||||||
|
|
||||||
private final MutableLiveData<String> mText;
|
|
||||||
|
|
||||||
public DashboardViewModel() {
|
|
||||||
mText = new MutableLiveData<>();
|
|
||||||
mText.setValue("This is dashboard fragment");
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<String> getText() {
|
|
||||||
return mText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.home;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentHomeBinding;
|
|
||||||
|
|
||||||
public class HomeFragment extends Fragment {
|
|
||||||
|
|
||||||
private FragmentHomeBinding binding;
|
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
HomeViewModel homeViewModel =
|
|
||||||
new ViewModelProvider(this).get(HomeViewModel.class);
|
|
||||||
|
|
||||||
binding = FragmentHomeBinding.inflate(inflater, container, false);
|
|
||||||
View root = binding.getRoot();
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.home;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
|
|
||||||
public class HomeViewModel extends ViewModel {
|
|
||||||
|
|
||||||
private final MutableLiveData<String> mText;
|
|
||||||
|
|
||||||
public HomeViewModel() {
|
|
||||||
mText = new MutableLiveData<>();
|
|
||||||
mText.setValue("This is home fragment");
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<String> getText() {
|
|
||||||
return mText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
|
|
||||||
import com.ppwwyyxx.wxgfdecoder.databinding.FragmentNotificationsBinding;
|
|
||||||
|
|
||||||
public class NotificationsFragment extends Fragment {
|
|
||||||
|
|
||||||
private FragmentNotificationsBinding binding;
|
|
||||||
|
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
|
||||||
ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
NotificationsViewModel notificationsViewModel =
|
|
||||||
new ViewModelProvider(this).get(NotificationsViewModel.class);
|
|
||||||
|
|
||||||
binding = FragmentNotificationsBinding.inflate(inflater, container, false);
|
|
||||||
View root = binding.getRoot();
|
|
||||||
|
|
||||||
final TextView textView = binding.textNotifications;
|
|
||||||
notificationsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
super.onDestroyView();
|
|
||||||
binding = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package com.ppwwyyxx.wxgfdecoder.ui.notifications;
|
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData;
|
|
||||||
import androidx.lifecycle.MutableLiveData;
|
|
||||||
import androidx.lifecycle.ViewModel;
|
|
||||||
|
|
||||||
public class NotificationsViewModel extends ViewModel {
|
|
||||||
|
|
||||||
private final MutableLiveData<String> mText;
|
|
||||||
|
|
||||||
public NotificationsViewModel() {
|
|
||||||
mText = new MutableLiveData<>();
|
|
||||||
mText.setValue("This is notifications fragment");
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiveData<String> getText() {
|
|
||||||
return mText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.tencent.mm.plugin.fts.jni;
|
|
||||||
|
|
||||||
public class FTSJNIUtils {
|
|
||||||
public static native int countTokens(String str);
|
|
||||||
|
|
||||||
public static native int icuInitialize(String str);
|
|
||||||
|
|
||||||
public static native String icuTokenizer(String str);
|
|
||||||
|
|
||||||
public static native void nativeInitFts(long j, byte[] bArr);
|
|
||||||
|
|
||||||
public static native int stringCompareUtfBinary(String str, String str2);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
//File:
|
|
||||||
package com.tencent.mm.plugin.gif;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
/*
|
|
||||||
*import com.tencent.matrix.trace.core.AppMethodBeat;
|
|
||||||
*import com.tencent.mm.loader.p558j.C11743b;
|
|
||||||
*import com.tencent.mm.sdk.platformtools.C52656ad;
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* renamed from: com.tencent.mm.plugin.gif.MMWXGFJNI */
|
|
||||||
public class MMWXGFJNI {
|
|
||||||
public static final int PIC_TYPE_JPG = 0;
|
|
||||||
public static final int PIC_TYPE_PNG = 1;
|
|
||||||
private static final String TAG = "MicroMsg.JNI.MMWXGF";
|
|
||||||
static int mECode;
|
|
||||||
public static boolean mIsInit;
|
|
||||||
|
|
||||||
|
|
||||||
public static native int nativeAV2Gif(String str, String str2);
|
|
||||||
|
|
||||||
public static native int nativeAV2Gif(byte[] bArr, byte[] bArr2);
|
|
||||||
|
|
||||||
public static native int nativeAddGifEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
|
|
||||||
|
|
||||||
public static native int nativeAddWxAMEncodeRgbaFrame(long j, int i, int i2, byte[] bArr, long j2);
|
|
||||||
|
|
||||||
public static native int nativeDecodeBufferFrame(long j, byte[] bArr, int i, Bitmap bitmap, int[] iArr);
|
|
||||||
|
|
||||||
public static native int nativeDecodeBufferHeader(long j, byte[] bArr, int i);
|
|
||||||
|
|
||||||
public static native byte[] nativeFinishGifEncode(long j);
|
|
||||||
|
|
||||||
public static native byte[] nativeFinishWxAMEncode(long j);
|
|
||||||
|
|
||||||
public static native int nativeGetOption(long j, byte[] bArr, int i, int[] iArr);
|
|
||||||
|
|
||||||
public static native int nativeInit(String str);
|
|
||||||
|
|
||||||
public static native long nativeInitGifEncode(int i, int i2, long j);
|
|
||||||
|
|
||||||
public static native long nativeInitWxAMDecoder();
|
|
||||||
|
|
||||||
public static native long nativeInitWxAMEncoder(int i, int i2, long j, int i3);
|
|
||||||
|
|
||||||
public static native boolean nativeIsWXGF(byte[] bArr, int i);
|
|
||||||
|
|
||||||
public static native int nativePic2Wxam(String str, String str2, int i, int i2);
|
|
||||||
|
|
||||||
public static native int nativePic2WxamWithWH(String str, String str2, int i, int i2, int i3, int i4);
|
|
||||||
|
|
||||||
public static native int nativePic2WxamWithWH(String str, byte[] bArr, int i, int i2, int i3, int i4, int i5, int i6, int i7);
|
|
||||||
|
|
||||||
public static native int nativeRewindBuffer(long j);
|
|
||||||
|
|
||||||
public static native int nativeUninit(long j);
|
|
||||||
|
|
||||||
public static native int nativeWxam2Pic(String str, String str2);
|
|
||||||
|
|
||||||
public static native byte[] nativeWxam2PicBuf(byte[] bArr);
|
|
||||||
|
|
||||||
public static native byte[] nativeWxamToGif(byte[] bArr);
|
|
||||||
|
|
||||||
public static void initialize(String nativeDir) {
|
|
||||||
// init wxgf decoder
|
|
||||||
String soFilePath = nativeDir + "/libvoipCodec.so";
|
|
||||||
File soFile = new File(soFilePath);
|
|
||||||
if (!soFile.exists())
|
|
||||||
throw new RuntimeException("missing so file " + soFilePath);
|
|
||||||
int initSuccess = nativeInit(soFilePath); // return -1 if missing so
|
|
||||||
if (initSuccess < 0)
|
|
||||||
throw new RuntimeException("nativeInit failed " + initSuccess);
|
|
||||||
mIsInit = true;
|
|
||||||
}
|
|
||||||
/* // Original code below:
|
|
||||||
*static {
|
|
||||||
* AppMethodBeat.m13231i(104681);
|
|
||||||
* mIsInit = false;
|
|
||||||
* mECode = -1;
|
|
||||||
* C52656ad.m88211i(TAG, "static MMWXGFJNI");
|
|
||||||
* if (!mIsInit) {
|
|
||||||
* String str = C11743b.ahY() + "lib/libvoipCodec.so";
|
|
||||||
* int nativeInit = nativeInit(str);
|
|
||||||
* mECode = nativeInit;
|
|
||||||
* mIsInit = nativeInit >= 0;
|
|
||||||
* C52656ad.m88212i(TAG, "native init MMWXGF mECode:%d result:%b :%s", Integer.valueOf(mECode), Boolean.valueOf(mIsInit), str);
|
|
||||||
* AppMethodBeat.m13232o(104681);
|
|
||||||
* return;
|
|
||||||
* }
|
|
||||||
* C52656ad.m88205d(TAG, "MMWXGF has init.");
|
|
||||||
* AppMethodBeat.m13232o(104681);
|
|
||||||
*}
|
|
||||||
*/
|
|
||||||
|
|
||||||
public static int getErrorCode() {
|
|
||||||
return mECode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_host_fragment_activity_main"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
app:defaultNavHost="true"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:navGraph="@navigation/mobile_navigation" />
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<Space
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="48dp" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/edit_text_port"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="Enter Port Number (default 8080)"
|
|
||||||
android:inputType="number" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_hostname"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="Hostname: " />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view_port"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:text="Port: " />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/start_server_button"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Start Server" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="400dp"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/log_text_area"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:scrollbars="vertical" />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <com.google.android.material.bottomnavigation.BottomNavigationView-->
|
|
||||||
<!-- android:id="@+id/nav_view"-->
|
|
||||||
<!-- android:layout_width="match_parent"-->
|
|
||||||
<!-- android:layout_height="wrap_content"-->
|
|
||||||
<!-- android:layout_marginStart="0dp"-->
|
|
||||||
<!-- android:layout_marginEnd="0dp"-->
|
|
||||||
<!-- android:layout_gravity="bottom"-->
|
|
||||||
<!-- android:background="?android:attr/windowBackground"-->
|
|
||||||
|
|
||||||
<!-- app:menu="@menu/bottom_nav_menu" />-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.dashboard.DashboardFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_dashboard"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.home.HomeFragment">
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.notifications.NotificationsFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_notifications"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navigation_home"
|
|
||||||
android:icon="@drawable/ic_home_black_24dp"
|
|
||||||
android:title="@string/title_home" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navigation_dashboard"
|
|
||||||
android:icon="@drawable/ic_dashboard_black_24dp"
|
|
||||||
android:title="@string/title_dashboard" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/navigation_notifications"
|
|
||||||
android:icon="@drawable/ic_notifications_black_24dp"
|
|
||||||
android:title="@string/title_notifications" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/mobile_navigation"
|
|
||||||
app:startDestination="@+id/navigation_home">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/navigation_home"
|
|
||||||
android:name="com.ppwwyyxx.wxgfdecoder.ui.home.HomeFragment"
|
|
||||||
android:label="@string/title_home"
|
|
||||||
tools:layout="@layout/fragment_home" />
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/navigation_dashboard"
|
|
||||||
android:name="com.ppwwyyxx.wxgfdecoder.ui.dashboard.DashboardFragment"
|
|
||||||
android:label="@string/title_dashboard"
|
|
||||||
tools:layout="@layout/fragment_dashboard" />
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/navigation_notifications"
|
|
||||||
android:name="com.ppwwyyxx.wxgfdecoder.ui.notifications.NotificationsFragment"
|
|
||||||
android:label="@string/title_notifications"
|
|
||||||
tools:layout="@layout/fragment_notifications" />
|
|
||||||
</navigation>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/black</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="purple_200">#FFBB86FC</color>
|
|
||||||
<color name="purple_500">#FF6200EE</color>
|
|
||||||
<color name="purple_700">#FF3700B3</color>
|
|
||||||
<color name="teal_200">#FF03DAC5</color>
|
|
||||||
<color name="teal_700">#FF018786</color>
|
|
||||||
<color name="black">#FF000000</color>
|
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
|
||||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
|
||||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
|
||||||
</resources>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">WXGF Decoder</string>
|
|
||||||
<string name="title_home">Home</string>
|
|
||||||
<string name="title_dashboard">Dashboard</string>
|
|
||||||
<string name="title_notifications">Notifications</string>
|
|
||||||
</resources>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<!-- Base application theme. -->
|
|
||||||
<style name="Theme.WXAMConverter" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<!-- Primary brand color. -->
|
|
||||||
<item name="colorPrimary">@color/purple_500</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
|
||||||
<item name="colorOnPrimary">@color/white</item>
|
|
||||||
<!-- Secondary brand color. -->
|
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<!-- Status bar color. -->
|
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
|
||||||
<!-- Customize your theme here. -->
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample backup rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/guide/topics/data/autobackup
|
|
||||||
for details.
|
|
||||||
Note: This file is ignored for devices older that API 31
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore
|
|
||||||
-->
|
|
||||||
<full-backup-content>
|
|
||||||
<!--
|
|
||||||
<include domain="sharedpref" path="."/>
|
|
||||||
<exclude domain="sharedpref" path="device.xml"/>
|
|
||||||
-->
|
|
||||||
</full-backup-content>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
Sample data extraction rules file; uncomment and customize as necessary.
|
|
||||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
|
||||||
for details.
|
|
||||||
-->
|
|
||||||
<data-extraction-rules>
|
|
||||||
<cloud-backup>
|
|
||||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
-->
|
|
||||||
</cloud-backup>
|
|
||||||
<!--
|
|
||||||
<device-transfer>
|
|
||||||
<include .../>
|
|
||||||
<exclude .../>
|
|
||||||
</device-transfer>
|
|
||||||
-->
|
|
||||||
</data-extraction-rules>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.android.application) apply false
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Project-wide Gradle settings.
|
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. For more details, visit
|
|
||||||
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
|
||||||
# thereby reducing the size of the R class for that library
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[versions]
|
|
||||||
agp = "8.8.0"
|
|
||||||
junit = "4.13.2"
|
|
||||||
junitVersion = "1.1.5"
|
|
||||||
espressoCore = "3.5.1"
|
|
||||||
appcompat = "1.6.1"
|
|
||||||
material = "1.10.0"
|
|
||||||
constraintlayout = "2.1.4"
|
|
||||||
lifecycleLivedataKtx = "2.6.1"
|
|
||||||
lifecycleViewmodelKtx = "2.6.1"
|
|
||||||
navigationFragment = "2.6.0"
|
|
||||||
navigationUi = "2.6.0"
|
|
||||||
media3Common = "1.5.1"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
||||||
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
|
||||||
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
|
||||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
|
||||||
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
|
||||||
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
|
||||||
lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" }
|
|
||||||
lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
|
|
||||||
navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "navigationFragment" }
|
|
||||||
navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "navigationUi" }
|
|
||||||
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#Wed Jan 22 21:22:48 PST 2025
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright 2015 the original author or authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
##
|
|
||||||
## Gradle start up script for UN*X
|
|
||||||
##
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
PRG="$0"
|
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
|
||||||
ls=`ls -ld "$PRG"`
|
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
|
||||||
PRG="$link"
|
|
||||||
else
|
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=`basename "$0"`
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD="maximum"
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "`uname`" in
|
|
||||||
CYGWIN* )
|
|
||||||
cygwin=true
|
|
||||||
;;
|
|
||||||
Darwin* )
|
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="java"
|
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
|
||||||
if [ $? -eq 0 ] ; then
|
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
ulimit -n $MAX_FD
|
|
||||||
if [ $? -ne 0 ] ; then
|
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Escape application args
|
|
||||||
save () {
|
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
|
||||||
echo " "
|
|
||||||
}
|
|
||||||
APP_ARGS=`save "$@"`
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%" == "" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%" == "" set DIRNAME=.
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
|
||||||
echo.
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
echo location of your Java installation.
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
Before Width: | Height: | Size: 85 KiB |
@@ -1,23 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google {
|
|
||||||
content {
|
|
||||||
includeGroupByRegex("com\\.android.*")
|
|
||||||
includeGroupByRegex("com\\.google.*")
|
|
||||||
includeGroupByRegex("androidx.*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "WXGFDecoder"
|
|
||||||
include(":app")
|
|
||||||
@@ -1,21 +1,48 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# File: android-interact.sh
|
# File: android-interact.sh
|
||||||
|
# Date: Fri Jun 26 10:38:07 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
|
PROG_NAME=`python -c "import os, sys; print(os.path.realpath(sys.argv[1]))" "$0"`
|
||||||
PROG_DIR=`dirname "$PROG_NAME"`
|
PROG_DIR=`dirname "$PROG_NAME"`
|
||||||
cd "$PROG_DIR"
|
cd "$PROG_DIR"
|
||||||
|
|
||||||
# Please check that your path is the same, since this might be different among devices
|
source compatibility.sh
|
||||||
# RES_DIR="/mnt/sdcard/tencent/MicroMsg" # old version of wechat use this path.
|
|
||||||
RES_DIR="/data/data/com.tencent.mm/MicroMsg"
|
|
||||||
|
|
||||||
if [[ $1 == "db" || $1 == "res" ]]; then
|
# Please check that your path is the same, since this might be different among devices
|
||||||
|
RES_DIR="/mnt/sdcard/tencent/MicroMsg"
|
||||||
|
MM_DIR="/data/data/com.tencent.mm"
|
||||||
|
|
||||||
|
echo "Starting rooted adb server..."
|
||||||
|
adb root
|
||||||
|
|
||||||
|
if [[ $1 == "uin" ]]; then
|
||||||
|
adb pull $MM_DIR/shared_prefs/system_config_prefs.xml 2>/dev/null
|
||||||
|
uin=$($GREP 'default_uin' system_config_prefs.xml | $GREP -o 'value="\-?[0-9]*' | cut -c 8-)
|
||||||
|
[[ -n $uin ]] || {
|
||||||
|
>&2 echo "Failed to get wechat uin. You can try other methods, or report a bug."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
rm system_config_prefs.xml
|
||||||
|
echo "Got wechat uin: $uin"
|
||||||
|
elif [[ $1 == "imei" ]]; then
|
||||||
|
imei=$(adb shell dumpsys iphonesubinfo | $GREP 'Device ID' | $GREP -o '[0-9]+')
|
||||||
|
[[ -n $imei ]] || {
|
||||||
|
imei=$(adb shell service call iphonesubinfo 1 | awk -F "'" '{print $2}' | sed 's/[^0-9A-F]*//g' | tr -d '\n')
|
||||||
|
}
|
||||||
|
[[ -n $imei ]] || {
|
||||||
|
>&2 echo "Failed to get imei. You can try other methods, or report a bug."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "Got imei: $imei"
|
||||||
|
elif [[ $1 == "db" || $1 == "res" ]]; then
|
||||||
echo "Looking for user dir name..."
|
echo "Looking for user dir name..."
|
||||||
# look for dirname which looks like md5 (32 alpha-numeric chars)
|
sleep 1 # sometimes adb complains: device not found
|
||||||
userList=$(adb shell su -c "ls $RES_DIR" | awk '{if (length() == 32) print}')
|
userList=$(adb ls $RES_DIR | cut -f 4 -d ' ' \
|
||||||
numUser=$(echo "$userList" | wc -l)
|
| awk '{if (length() == 32) print}')
|
||||||
|
numUser=$(echo $userList | wc -l)
|
||||||
# choose the first user.
|
# choose the first user.
|
||||||
chooseUser=$(echo "$userList" | head -n1)
|
chooseUser=$(echo $userList | head -n1)
|
||||||
[[ -n $chooseUser ]] || {
|
[[ -n $chooseUser ]] || {
|
||||||
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
|
>&2 echo "Could not find user. Please check whether your resource dir is $RES_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -23,61 +50,54 @@ if [[ $1 == "db" || $1 == "res" ]]; then
|
|||||||
echo "Found $numUser user(s). User chosen: $chooseUser"
|
echo "Found $numUser user(s). User chosen: $chooseUser"
|
||||||
|
|
||||||
if [[ $1 == "res" ]]; then
|
if [[ $1 == "res" ]]; then
|
||||||
mkdir -p resource
|
echo "Pulling resources... this might take a long time, because adb sucks..."
|
||||||
(
|
mkdir -p resource; cd resource
|
||||||
cd resource || exit
|
for d in image2 voice2 emoji video sfs; do
|
||||||
echo "Pulling resources... "
|
mkdir -p $d; cd $d
|
||||||
for d in avatar image2 voice2 emoji video sfs; do
|
adb pull $RES_DIR/$chooseUser/$d
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$d with busybox ..."
|
cd ..
|
||||||
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null |
|
[[ -d $d ]] || {
|
||||||
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
>&2 echo "Failed to download resource directory: $RES_DIR/$chooseUser/$d"
|
||||||
[[ -d $d ]] && continue
|
exit 1
|
||||||
|
}
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$d with tar & base64 ..."
|
done
|
||||||
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$d 2>/dev/null | base64" |
|
cd ..
|
||||||
base64 -di | tar xzf - --strip-components 5
|
echo "Resource pulled at ./resource"
|
||||||
[[ -d $d ]] && continue
|
echo "Total size: $(du -sh resource | cut -f1)"
|
||||||
|
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$d with adb pull (slow) ..."
|
|
||||||
mkdir -p $d
|
|
||||||
(
|
|
||||||
cd $d || exit
|
|
||||||
adb root
|
|
||||||
sleep 1 # sometimes adb complains: device not found
|
|
||||||
adb pull "$RES_DIR/$chooseUser/$d"
|
|
||||||
)
|
|
||||||
|
|
||||||
[[ -d $d ]] || {
|
|
||||||
echo "Failed to download $RES_DIR/$chooseUser/$d"
|
|
||||||
}
|
|
||||||
done
|
|
||||||
echo "Resource pulled at ./resource"
|
|
||||||
echo "Total size: $(du -sh | cut -f1)"
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
for f in EnMicroMsg.db sfs/avatar.index; do
|
echo "Pulling database and avatar index file..."
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$f with busybox ..."
|
adb pull $MM_DIR/MicroMsg/$chooseUser/EnMicroMsg.db
|
||||||
adb shell su -c "busybox tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null |
|
[[ -f EnMicroMsg.db ]] && \
|
||||||
busybox base64" | base64 -di | tar xzf - --strip-components 5
|
echo "Database successfully downloaded to EnMicroMsg.db" || {
|
||||||
[[ -f $f ]] && continue
|
>&2 echo "Failed to pull database by adb"
|
||||||
|
exit 1
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$f with tar & base64 ..."
|
}
|
||||||
adb shell su -c "tar czf - $RES_DIR/$chooseUser/$f 2>/dev/null | base64" |
|
adb pull $MM_DIR/MicroMsg/$chooseUser/sfs/avatar.index
|
||||||
base64 -di | tar xzf - --strip-components 5
|
[[ -f avatar.index ]] && \
|
||||||
[[ -f $f ]] && continue
|
echo "Avatar index successfully downloaded to avatar.index" || {
|
||||||
|
>&2 echo "Failed to pull avatar index by adb, are you using latest version of wechat?"
|
||||||
echo "Trying to download $RES_DIR/$chooseUser/$f with adb pull..."
|
exit 1
|
||||||
adb root
|
|
||||||
sleep 1
|
|
||||||
adb pull $RES_DIR/$chooseUser/$f
|
|
||||||
|
|
||||||
[[ -f $f ]] || {
|
|
||||||
echo "Failed to download $RES_DIR/$chooseUser/$f"
|
|
||||||
}
|
}
|
||||||
done
|
|
||||||
echo "Database and avatar index file successfully downloaded"
|
|
||||||
fi
|
fi
|
||||||
|
elif [[ $1 == "db-decrypt" ]]; then
|
||||||
|
echo "Getting uin..."
|
||||||
|
$0 uin | tail -n1 | $GREP -o '\-?[0-9]*' | tee /tmp/uin
|
||||||
|
echo "Getting imei..."
|
||||||
|
$0 imei | tail -n1 | $GREP -o '[0-9]*' | tee /tmp/imei
|
||||||
|
echo "Getting db..."
|
||||||
|
$0 db
|
||||||
|
echo "Decrypting db..."
|
||||||
|
imei=$(cat /tmp/imei)
|
||||||
|
uin=$(cat /tmp/uin)
|
||||||
|
if [[ -z $imei || -z $uin ]]; then
|
||||||
|
>&2 echo "Failed to get imei or uin. See README for manual methods."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
./decrypt-db.py EnMicroMsg.db $imei $uin
|
||||||
|
rm /tmp/{uin,imei}
|
||||||
|
echo "Done. See decrypted.db"
|
||||||
else
|
else
|
||||||
echo "Usage: $0 <res|db>"
|
echo "Usage: $0 <res|db-decrypt>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: progress.py
|
||||||
|
# Date: Wed Jun 17 23:59:52 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
@@ -32,12 +36,12 @@ class ProgressReporter(object):
|
|||||||
self._prev_report_time = now
|
self._prev_report_time = now
|
||||||
dt = now - self._start_time
|
dt = now - self._start_time
|
||||||
if self._total and self._cnt > 0:
|
if self._total and self._cnt > 0:
|
||||||
eta_msg = '{}/{} ETA: {:.1f}'.format(self._cnt, self._total,
|
eta_msg = '{}/{} ETA: {:.2f}'.format(self._cnt, self._total,
|
||||||
(self._total-self._cnt)*dt/self._cnt)
|
(self._total-self._cnt)*dt/self._cnt)
|
||||||
else:
|
else:
|
||||||
eta_msg = '{} done'.format(self._cnt)
|
eta_msg = '{} done'.format(self._cnt)
|
||||||
self._fout.write(u'{}: avg {:.1f}/sec'
|
self._fout.write(u'{}: avg {:.3f}/sec'
|
||||||
u', passed {:.1f}sec, {} {} \r'.format(
|
u', passed {:.3f}sec, {} {} \r'.format(
|
||||||
self._name, self._cnt / dt, dt, eta_msg, extra_msg))
|
self._name, self._cnt / dt, dt, eta_msg, extra_msg))
|
||||||
self._fout.flush()
|
self._fout.flush()
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: utils.py
|
||||||
|
# Date: Wed Jun 17 23:59:25 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def ensure_bin_str(s):
|
||||||
|
if type(s) == str:
|
||||||
|
return s
|
||||||
|
if type(s) == unicode:
|
||||||
|
return s.encode('utf-8')
|
||||||
|
|
||||||
|
def ensure_unicode(s):
|
||||||
|
if type(s) == str:
|
||||||
|
return s.decode('utf-8')
|
||||||
|
if type(s) == unicode:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def md5(s):
|
||||||
|
m = hashlib.md5()
|
||||||
|
m.update(s)
|
||||||
|
return m.hexdigest()
|
||||||
|
|
||||||
|
def get_file_b64(fname):
|
||||||
|
data = open(fname, 'rb').read()
|
||||||
|
return base64.b64encode(data)
|
||||||
|
|
||||||
|
def safe_filename(fname):
|
||||||
|
filename = ensure_unicode(fname)
|
||||||
|
return "".join(
|
||||||
|
[c for c in filename if c.isalpha() or c.isdigit() or c==' ']).rstrip()
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: timer.py
|
||||||
|
# Date: Wed Jun 17 23:25:54 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import time, functools
|
import time, functools
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -16,7 +20,7 @@ class TotalTimer(object):
|
|||||||
self.times = defaultdict(float)
|
self.times = defaultdict(float)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
for k, v in self.times.items():
|
for k, v in self.times.iteritems():
|
||||||
logger.info("{} took {} seconds in total.".format(k, v))
|
logger.info("{} took {} seconds in total.".format(k, v))
|
||||||
|
|
||||||
_total_timer = TotalTimer()
|
_total_timer = TotalTimer()
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
# $File: compatibility.sh
|
||||||
|
# $Date: Tue Jun 16 22:23:36 2015 +0800
|
||||||
|
# Author: Vury Leo <i[at]vuryleo[dot]com>
|
||||||
|
|
||||||
|
if [ `uname` = 'Darwin' ]; then
|
||||||
|
GREP='grep -E'
|
||||||
|
MD5SUM='md5'
|
||||||
|
else
|
||||||
|
GREP='grep -E'
|
||||||
|
MD5SUM='md5sum'
|
||||||
|
fi
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
# File: count-message.sh
|
# File: count-message.sh
|
||||||
# Date: Wed Nov 29 02:32:40 2017 -0800
|
# Date: Sun Apr 12 21:01:01 2015 +0900
|
||||||
# Author: Kangjing Huang <[email protected]>
|
# Author: Kangjing Huang <[email protected]>
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ then
|
|||||||
fi
|
fi
|
||||||
# TODO work on db directly
|
# TODO work on db directly
|
||||||
|
|
||||||
echo -e "Filename\t#Lines\t#Chars\t#Words"
|
echo -e "Filename\tCounts of message\tCounts of chars\tCounts of words"
|
||||||
|
|
||||||
SAVEIFS=$IFS
|
SAVEIFS=$IFS
|
||||||
IFS=$(echo -en "\n\b")
|
IFS=$(echo -en "\n\b")
|
||||||
@@ -23,6 +23,6 @@ do
|
|||||||
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
|
CHARCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -m)
|
||||||
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
|
WORDCOUNT=$(cat "$i"| sed 's/.*:[0-9][0-9]:\(.*\)/\1/g' | sed 's/\[.*\]//g' | grep -v img | wc -w)
|
||||||
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
|
echo -e "$LINECOUNT\t$CHARCOUNT\t$WORDCOUNT"
|
||||||
done | sort -t $'\t' -k 2 -n | column -t -s $'\t'
|
done
|
||||||
|
|
||||||
IFS=$SAVEIFS
|
IFS=$SAVEIFS
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: decrypt-db.py
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from pysqlcipher import dbapi2 as sqlite
|
||||||
|
|
||||||
|
from hashlib import md5
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
DEFAULT_OUTPUT_DB_NAME = 'decrypted.db'
|
||||||
|
def get_args():
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument('db', help='path to EnMicroMsg.db')
|
||||||
|
parser.add_argument('imei', help='15 digit IMEI of your phone')
|
||||||
|
parser.add_argument('uin', help='WeChat UIN')
|
||||||
|
parser.add_argument('--output', help='output decrypted database',
|
||||||
|
default=DEFAULT_OUTPUT_DB_NAME)
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
def get_key(imei, uin):
|
||||||
|
a = md5(imei + uin)
|
||||||
|
return a.hexdigest()[:7]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = get_args()
|
||||||
|
|
||||||
|
output = args.output
|
||||||
|
if os.path.abspath(os.path.dirname(output)) != os.path.abspath('.'):
|
||||||
|
print "Output file must be in current directory"
|
||||||
|
sys.exit(1)
|
||||||
|
if os.path.isfile(output):
|
||||||
|
print "{} already exists. Remove? (y/n)".format(args.output),
|
||||||
|
ans = raw_input()
|
||||||
|
if ans not in ['y', 'Y']:
|
||||||
|
print "Bye!"
|
||||||
|
sys.exit()
|
||||||
|
os.unlink(output)
|
||||||
|
|
||||||
|
key = get_key(args.imei, args.uin)
|
||||||
|
print "KEY: {}".format(key)
|
||||||
|
|
||||||
|
print "Decrypt and dump database to {} ... ".format(output)
|
||||||
|
conn = sqlite.connect(args.db)
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("PRAGMA key = '" + key + "';")
|
||||||
|
c.execute("PRAGMA cipher_use_hmac = OFF;")
|
||||||
|
c.execute("PRAGMA cipher_page_size = 1024;")
|
||||||
|
c.execute("PRAGMA kdf_iter = 4000;")
|
||||||
|
c.execute("ATTACH DATABASE '" + args.output + "' AS db KEY '';")
|
||||||
|
c.execute("SELECT sqlcipher_export('db');" )
|
||||||
|
c.execute("DETACH DATABASE db;" )
|
||||||
|
c.close()
|
||||||
@@ -1,44 +1,49 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# File: dump-audio.py
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import base64
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
from common.textutil import ensure_unicode
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from wechat.msg import TYPE_SPEAK
|
|
||||||
from wechat.res import Resource
|
from wechat.res import Resource
|
||||||
|
from wechat.render import HTMLRender
|
||||||
|
from wechat.libchathelper import LibChatHelper
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('name', help='name of contact')
|
parser.add_argument('name', help='name of contact')
|
||||||
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
|
parser.add_argument('--output', help='output mp3 dir', default='/tmp')
|
||||||
parser.add_argument('--db', default='decoded.db', help='path to decoded database')
|
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
|
||||||
parser.add_argument('--res', default='resource', help='reseource directory')
|
parser.add_argument('--res', default='resource', help='reseource directory')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = get_args()
|
args = get_args()
|
||||||
|
|
||||||
|
name = ensure_unicode(args.name)
|
||||||
|
output_file = args.output
|
||||||
|
|
||||||
parser = WeChatDBParser(args.db)
|
parser = WeChatDBParser(args.db)
|
||||||
res = Resource(parser, args.res)
|
res = Resource(args.res, '')
|
||||||
|
|
||||||
try:
|
if name and name in parser.msgs_by_chat:
|
||||||
chatid = parser.get_chat_id(args.name)
|
msgs = parser.msgs_by_chat[name]
|
||||||
except KeyError:
|
else:
|
||||||
sys.stderr.write(u"Valid Contacts: {}\n".format('\n'.join(parser.all_chat_nicknames)))
|
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
||||||
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
print "Number of Messages: ", len(msgs)
|
||||||
msgs = parser.msgs_by_chat[chatid]
|
|
||||||
print(f"Number of Messages for {args.name}: ", len(msgs))
|
|
||||||
assert len(msgs) > 0
|
assert len(msgs) > 0
|
||||||
|
|
||||||
voice_msgs = [m for m in msgs if m.type == TYPE_SPEAK]
|
libchat = LibChatHelper(parser, res)
|
||||||
for idx, m in enumerate(voice_msgs):
|
msgs = libchat.convert_msgs(msgs)
|
||||||
audio_str, duration = res.get_voice_mp3(m.imgPath)
|
voices = [m.sound for m in msgs if m.sound]
|
||||||
audio_bytes = base64.b64decode(audio_str)
|
for idx, v in enumerate(voices):
|
||||||
outf = f'/{args.output}/{idx:04d}-{duration:.1f}s.mp3'
|
p = v.find(':')
|
||||||
with open(outf, 'wb') as f:
|
v = v[p:]
|
||||||
f.write(audio_bytes)
|
with open('/{}/{:04d}.mp3'.format(args.output, idx), 'wb') as f:
|
||||||
print(f"Audio written to {outf}")
|
f.write(v)
|
||||||
|
|||||||
@@ -1,66 +1,52 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
import os
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: dump-html.py
|
||||||
|
# Date: Wed Mar 25 17:44:20 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
from common.textutil import ensure_unicode
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from wechat.res import Resource
|
from wechat.res import Resource
|
||||||
from wechat.render import HTMLRender
|
from wechat.render import HTMLRender
|
||||||
|
|
||||||
logger = logging.getLogger("wechat")
|
|
||||||
|
|
||||||
def get_args():
|
def get_args():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('name', help='name of contact')
|
parser.add_argument('name', help='name of contact')
|
||||||
parser.add_argument('--output', help='output html file, e.g. output.html', default='output.html')
|
parser.add_argument('--output', help='output html file', default='output.html')
|
||||||
parser.add_argument('--db', default='EnMicroMsg.db.decoded',
|
parser.add_argument('--db', default='decrypted.db', help='path to decrypted database')
|
||||||
help='path to the decoded database, e.g. EnMicroMsg.db.decoded')
|
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file')
|
||||||
parser.add_argument('--res', default='resource', help='the resource directory')
|
parser.add_argument('--res', default='resource', help='reseource directory')
|
||||||
parser.add_argument('--wxgf-server', help='address of the wxgf image decoder server')
|
|
||||||
parser.add_argument('--avt', default='avatar.index', help='path to avatar.index file that only exists in old version of wechat. Ignore for new version of wechat.')
|
|
||||||
parser.add_argument('--start', help='start time in format of YYYY-MM-DD HH:MM:SS',
|
|
||||||
type=datetime.fromisoformat)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
return args
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = get_args()
|
args = get_args()
|
||||||
|
|
||||||
|
name = ensure_unicode(args.name)
|
||||||
output_file = args.output
|
output_file = args.output
|
||||||
|
|
||||||
parser = WeChatDBParser(args.db)
|
parser = WeChatDBParser(args.db)
|
||||||
|
res = Resource(parser, args.res, args.avt)
|
||||||
|
|
||||||
try:
|
if name and name in parser.msgs_by_chat:
|
||||||
chatid = parser.get_chat_id(args.name)
|
msgs = parser.msgs_by_chat[name]
|
||||||
except KeyError:
|
else:
|
||||||
sys.stderr.write(u"Valid Contacts: {}\n".format(
|
sys.stderr.write(u"Valid Contacts: {}\n".format(u'\n'.join(parser.msgs_by_chat.keys())))
|
||||||
u'\n'.join(parser.all_chat_nicknames)))
|
sys.stderr.write(u"Couldn't find that contact {}.".format(name));
|
||||||
sys.stderr.write(u"Couldn't find the chat {}.".format(args.name));
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
print "Number of Messages: ", len(msgs)
|
||||||
res = Resource(parser, args.res,
|
|
||||||
wxgf_server=args.wxgf_server,
|
|
||||||
avt_db=args.avt)
|
|
||||||
msgs = parser.msgs_by_chat[chatid]
|
|
||||||
logger.info(f"Number of Messages for chatid {chatid}: {len(msgs)}")
|
|
||||||
assert len(msgs) > 0
|
assert len(msgs) > 0
|
||||||
if args.start is not None:
|
|
||||||
msgs = [msg for msg in msgs if msg.createTime > args.start]
|
|
||||||
logger.info(f"Number of Messages after {args.start}: {len(msgs)}")
|
|
||||||
|
|
||||||
render = HTMLRender(parser, res)
|
render = HTMLRender(parser, res)
|
||||||
htmls = render.render_msgs(msgs)
|
htmls = render.render_msgs(msgs)
|
||||||
|
|
||||||
os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)
|
|
||||||
if len(htmls) == 1:
|
if len(htmls) == 1:
|
||||||
with open(output_file, 'w') as f:
|
with open(output_file, 'w') as f:
|
||||||
f.write(htmls[0])
|
print >> f, htmls[0].encode('utf-8')
|
||||||
else:
|
else:
|
||||||
assert output_file.endswith(".html")
|
|
||||||
basename = output_file[:-5]
|
|
||||||
for idx, html in enumerate(htmls):
|
for idx, html in enumerate(htmls):
|
||||||
with open(basename + f'{idx:02d}.html', 'w') as f:
|
with open(output_file + '.{}'.format(idx), 'w') as f:
|
||||||
f.write(html)
|
print >> f, html.encode('utf-8')
|
||||||
res.emoji_reader.flush_cache()
|
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: dump-msg.py
|
||||||
|
# Date: Mon May 25 15:23:05 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import logging
|
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
from wechat.common.textutil import safe_filename
|
from common.textutil import safe_filename
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
logger = logging.getLogger("wechat")
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit("Usage: {0} <path to decrypted_database.db> <output_dir>".format(sys.argv[0]))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
db_file = sys.argv[1]
|
||||||
if len(sys.argv) != 3:
|
output_dir = sys.argv[2]
|
||||||
sys.exit("Usage: {0} <path to decoded_database.db> <output_dir>".format(sys.argv[0]))
|
try:
|
||||||
|
os.mkdir(output_dir)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not os.path.isdir(output_dir):
|
||||||
|
sys.exit("Error creating directory {}".format(output_dir))
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
parser = WeChatDBParser(db_file)
|
||||||
output_dir = sys.argv[2]
|
|
||||||
try:
|
|
||||||
os.mkdir(output_dir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if not os.path.isdir(output_dir):
|
|
||||||
sys.exit("Error creating directory {}".format(output_dir))
|
|
||||||
|
|
||||||
parser = WeChatDBParser(db_file)
|
for name, msgs in parser.msgs_by_chat.iteritems():
|
||||||
|
print u"Writing msgs for {}".format(name)
|
||||||
for chatid, msgs in parser.msgs_by_chat.items():
|
safe_name = safe_filename(name)
|
||||||
name = parser.contacts[chatid]
|
with open(os.path.join(output_dir, safe_name + '.txt'), 'w') as f:
|
||||||
if len(name) == 0:
|
for m in msgs:
|
||||||
logger.info(f"Chat {chatid} doesn't have a valid display name.")
|
print >> f, m
|
||||||
name = str(id(chatid))
|
|
||||||
logger.info(f"Writing msgs for {name}")
|
|
||||||
safe_name = safe_filename(name)
|
|
||||||
outf = os.path.join(output_dir, safe_name + '.txt')
|
|
||||||
if os.path.isfile(outf):
|
|
||||||
logger.info(f"File {outf} exists! Skip contact {name}")
|
|
||||||
continue
|
|
||||||
with open(outf, 'w') as f:
|
|
||||||
for m in msgs:
|
|
||||||
f.write(str(m))
|
|
||||||
f.write("\n")
|
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import pickle
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import imghdr
|
|
||||||
import base64
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print("""\
|
|
||||||
Usage:
|
|
||||||
{} unpack output-dir
|
|
||||||
{} pack input-dir
|
|
||||||
""".format(sys.argv[0], sys.argv[0]))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if sys.argv[1] == 'unpack':
|
|
||||||
with open('emoji.cache', 'rb') as f:
|
|
||||||
dic = pickle.load(f)
|
|
||||||
outdir = sys.argv[2]
|
|
||||||
assert os.path.isdir(outdir)
|
|
||||||
for md5, img in dic.items():
|
|
||||||
data = img[0]
|
|
||||||
if not isinstance(data, bytes):
|
|
||||||
data = data.encode('ascii')
|
|
||||||
name = os.path.join(outdir, md5 + '.' + img[1].lower())
|
|
||||||
print(name)
|
|
||||||
with open(name, 'wb') as f:
|
|
||||||
f.write(base64.decodebytes(data))
|
|
||||||
elif sys.argv[1] == 'pack':
|
|
||||||
ret = {}
|
|
||||||
indir = sys.argv[2]
|
|
||||||
files = os.listdir(indir)
|
|
||||||
for fname in files:
|
|
||||||
try:
|
|
||||||
md5, format = fname.split('.')
|
|
||||||
except:
|
|
||||||
print("Unable to parse", fname)
|
|
||||||
continue
|
|
||||||
with open(os.path.join(indir, fname), 'rb') as f:
|
|
||||||
b64 = base64.encodebytes(f.read()).decode('ascii')
|
|
||||||
ret[md5] = (b64, format)
|
|
||||||
with open('emoji.cache', 'wb') as f:
|
|
||||||
pickle.dump(ret, f)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#!/bin/bash -e
|
|
||||||
# $File: compatibility.sh
|
|
||||||
# $Date: Tue Jun 16 22:23:36 2015 +0800
|
|
||||||
# Author: Vury Leo <i[at]vuryleo[dot]com>
|
|
||||||
|
|
||||||
if [ `uname` = 'Darwin' ]; then
|
|
||||||
GREP='grep -E'
|
|
||||||
MD5SUM='md5'
|
|
||||||
else
|
|
||||||
GREP='grep -E'
|
|
||||||
MD5SUM='md5sum'
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../compatibility.sh
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash -e
|
||||||
|
# File: decrypt-db.sh
|
||||||
|
# Date: Tue Jun 16 22:23:13 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
|
source compatibility.sh
|
||||||
|
|
||||||
|
MSGDB=$1
|
||||||
|
imei=$2
|
||||||
|
uin=$3
|
||||||
|
output=decrypted.db
|
||||||
|
|
||||||
|
if [[ -z "$1" || -z "$2" || -z "$3" ]]; then
|
||||||
|
echo "Usage: $0 <path to EnMicroMsg.db> <imei> <uin>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f $output ]]; then
|
||||||
|
echo -n "$output already exists. removed? (y/n)"
|
||||||
|
read r
|
||||||
|
[[ $r == "y" ]] && rm -v $output || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
KEY=$(echo -n "$imei$uin" | $MD5SUM | cut -b 1-7)
|
||||||
|
echo "KEY: $KEY"
|
||||||
|
|
||||||
|
uname | grep Darwin > /dev/null && os=darwin || os=linux
|
||||||
|
uname -m | grep x86_64 > /dev/null && version=64bit || version=32bit
|
||||||
|
echo "Use $version sqlcipher of $os."
|
||||||
|
|
||||||
|
echo "Dump decrypted database... (Don't worry about libcrypt.so version warning.)"
|
||||||
|
|
||||||
|
|
||||||
|
SQLCIPHER=./sqlcipher/$os/$version
|
||||||
|
export LD_LIBRARY_PATH=$SQLCIPHER
|
||||||
|
"$SQLCIPHER"/sqlcipher "$MSGDB" << EOF
|
||||||
|
PRAGMA key='$KEY';
|
||||||
|
PRAGMA cipher_use_hmac = off;
|
||||||
|
PRAGMA cipher_page_size = 1024;
|
||||||
|
PRAGMA kdf_iter = 4000;
|
||||||
|
ATTACH DATABASE "$output" AS db KEY "";
|
||||||
|
SELECT sqlcipher_export("db");
|
||||||
|
DETACH DATABASE db;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Database successfully dumped to $output"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
Copyright (c) 2008, ZETETIC LLC
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of the ZETETIC LLC nor the
|
||||||
|
names of its contributors may be used to endorse or promote products
|
||||||
|
derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
|
||||||
|
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
|
||||||
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
## NOTE
|
||||||
|
|
||||||
|
For convenience, I include the binary version of sqlcipher 2.1.1.
|
||||||
|
|
||||||
|
User can also choose to download and install sqlcipher 2.1.1 from their trusted source, such as:
|
||||||
|
https://launchpad.net/ubuntu/+source/sqlcipher/2.1.1-2
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
# sqlite3 - temporary wrapper script for .libs/sqlite3
|
||||||
|
# Generated by ltmain.sh (GNU libtool) 2.2.6
|
||||||
|
#
|
||||||
|
# The sqlite3 program cannot be directly executed until all the libtool
|
||||||
|
# libraries that it depends on are installed.
|
||||||
|
#
|
||||||
|
# This wrapper script should never be moved out of the build directory.
|
||||||
|
# If it is, it will not operate correctly.
|
||||||
|
|
||||||
|
# Sed substitution that helps us do robust quoting. It backslashifies
|
||||||
|
# metacharacters that are still active within double-quoted strings.
|
||||||
|
Xsed='/opt/local/bin/gsed -e 1s/^X//'
|
||||||
|
sed_quote_subst='s/\([`"$\\]\)/\\\1/g'
|
||||||
|
|
||||||
|
# Be Bourne compatible
|
||||||
|
if test -n "${ZSH_VERSION+set}" && (emulate sh) >/dev/null 2>&1; then
|
||||||
|
emulate sh
|
||||||
|
NULLCMD=:
|
||||||
|
# Zsh 3.x and 4.x performs word splitting on ${1+"$@"}, which
|
||||||
|
# is contrary to our usage. Disable this feature.
|
||||||
|
alias -g '${1+"$@"}'='"$@"'
|
||||||
|
setopt NO_GLOB_SUBST
|
||||||
|
else
|
||||||
|
case `(set -o) 2>/dev/null` in *posix*) set -o posix;; esac
|
||||||
|
fi
|
||||||
|
BIN_SH=xpg4; export BIN_SH # for Tru64
|
||||||
|
DUALCASE=1; export DUALCASE # for MKS sh
|
||||||
|
|
||||||
|
# The HP-UX ksh and POSIX shell print the target directory to stdout
|
||||||
|
# if CDPATH is set.
|
||||||
|
(unset CDPATH) >/dev/null 2>&1 && unset CDPATH
|
||||||
|
|
||||||
|
relink_command=""
|
||||||
|
|
||||||
|
# This environment variable determines our operation mode.
|
||||||
|
if test "$libtool_install_magic" = "%%%MAGIC variable%%%"; then
|
||||||
|
# install mode needs the following variables:
|
||||||
|
generated_by_libtool_version='2.2.6'
|
||||||
|
notinst_deplibs=' libsqlite3.la'
|
||||||
|
else
|
||||||
|
# When we are sourced in execute mode, $file and $ECHO are already set.
|
||||||
|
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
|
||||||
|
ECHO="/bin/echo"
|
||||||
|
file="$0"
|
||||||
|
# Make sure echo works.
|
||||||
|
if test "X$1" = X--no-reexec; then
|
||||||
|
# Discard the --no-reexec flag, and continue.
|
||||||
|
shift
|
||||||
|
elif test "X`{ $ECHO '\t'; } 2>/dev/null`" = 'X\t'; then
|
||||||
|
# Yippee, $ECHO works!
|
||||||
|
:
|
||||||
|
else
|
||||||
|
# Restart under the correct shell, and then maybe $ECHO will work.
|
||||||
|
exec /bin/sh "$0" --no-reexec ${1+"$@"}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the directory that this script lives in.
|
||||||
|
thisdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
|
||||||
|
test "x$thisdir" = "x$file" && thisdir=.
|
||||||
|
|
||||||
|
# Follow symbolic links until we get to the real thisdir.
|
||||||
|
file=`ls -ld "$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
|
||||||
|
while test -n "$file"; do
|
||||||
|
destdir=`$ECHO "X$file" | $Xsed -e 's%/[^/]*$%%'`
|
||||||
|
|
||||||
|
# If there was a directory component, then change thisdir.
|
||||||
|
if test "x$destdir" != "x$file"; then
|
||||||
|
case "$destdir" in
|
||||||
|
[\\/]* | [A-Za-z]:[\\/]*) thisdir="$destdir" ;;
|
||||||
|
*) thisdir="$thisdir/$destdir" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
file=`$ECHO "X$file" | $Xsed -e 's%^.*/%%'`
|
||||||
|
file=`ls -ld "$thisdir/$file" | /opt/local/bin/gsed -n 's/.*-> //p'`
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# Usually 'no', except on cygwin/mingw when embedded into
|
||||||
|
# the cwrapper.
|
||||||
|
WRAPPER_SCRIPT_BELONGS_IN_OBJDIR=no
|
||||||
|
if test "$WRAPPER_SCRIPT_BELONGS_IN_OBJDIR" = "yes"; then
|
||||||
|
# special case for '.'
|
||||||
|
if test "$thisdir" = "."; then
|
||||||
|
thisdir=`pwd`
|
||||||
|
fi
|
||||||
|
# remove .libs from thisdir
|
||||||
|
case "$thisdir" in
|
||||||
|
*[\\/].libs ) thisdir=`$ECHO "X$thisdir" | $Xsed -e 's%[\\/][^\\/]*$%%'` ;;
|
||||||
|
.libs ) thisdir=. ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to get the absolute directory name.
|
||||||
|
absdir=`cd "$thisdir" && pwd`
|
||||||
|
test -n "$absdir" && thisdir="$absdir"
|
||||||
|
|
||||||
|
program='sqlite3'
|
||||||
|
progdir="$thisdir/.libs"
|
||||||
|
|
||||||
|
|
||||||
|
if test -f "$progdir/$program"; then
|
||||||
|
# Add our own library path to DYLD_LIBRARY_PATH
|
||||||
|
DYLD_LIBRARY_PATH="/Users/vuryleo/Projects/sqlcipher/.libs:$DYLD_LIBRARY_PATH"
|
||||||
|
|
||||||
|
# Some systems cannot cope with colon-terminated DYLD_LIBRARY_PATH
|
||||||
|
# The second colon is a workaround for a bug in BeOS R4 sed
|
||||||
|
DYLD_LIBRARY_PATH=`$ECHO "X$DYLD_LIBRARY_PATH" | $Xsed -e 's/::*$//'`
|
||||||
|
|
||||||
|
export DYLD_LIBRARY_PATH
|
||||||
|
|
||||||
|
if test "$libtool_execute_magic" != "%%%MAGIC variable%%%"; then
|
||||||
|
# Run the actual program with our arguments.
|
||||||
|
|
||||||
|
exec "$progdir/$program" ${1+"$@"}
|
||||||
|
|
||||||
|
$ECHO "$0: cannot exec $program $*" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# The program doesn't exist.
|
||||||
|
$ECHO "$0: error: \`$progdir/$program' does not exist" 1>&2
|
||||||
|
$ECHO "This script is just a wrapper for $program." 1>&2
|
||||||
|
/bin/echo "See the libtool documentation for more information." 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: create_table.py
|
# File: create_table.py
|
||||||
# Date: Wed Mar 25 16:43:22 2015 +0800
|
# Date: Wed Mar 25 16:43:22 2015 +0800
|
||||||
# Author: Yuxin Wu
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -10,7 +10,7 @@ import os
|
|||||||
from libchat import SqliteLibChat
|
from libchat import SqliteLibChat
|
||||||
|
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print("Usage: {} <DB file name>")
|
print "Usage: {} <DB file name>"
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
db_name = sys.argv[1]
|
db_name = sys.argv[1]
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: libchat.py
|
||||||
|
# Date: Sun Apr 12 21:08:51 2015 +0900
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -96,7 +99,7 @@ class SqliteLibChat(object):
|
|||||||
else:
|
else:
|
||||||
self.c.execute("SELECT * FROM message WHERE {}".format(
|
self.c.execute("SELECT * FROM message WHERE {}".format(
|
||||||
' AND '.join(["{} = {}".format(k, v)
|
' AND '.join(["{} = {}".format(k, v)
|
||||||
for k, v in predicate.items()])))
|
for k, v in predicate.iteritems()])))
|
||||||
for row in self.c.fetchall():
|
for row in self.c.fetchall():
|
||||||
yield ChatMsg(*SqliteLibChat.postfilter(row))
|
yield ChatMsg(*SqliteLibChat.postfilter(row))
|
||||||
|
|
||||||
@@ -110,5 +113,5 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
for k in db.iterate_all_msg():
|
for k in db.iterate_all_msg():
|
||||||
from IPython import embed; embed()
|
from IPython import embed; embed()
|
||||||
print(k)
|
print k
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
# File: list-chats.py
|
# File: list-chats.py
|
||||||
# Author: Yuxin Wu <[email protected]>
|
# Author: Yuxin Wu <[email protected]>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
import sys
|
import sys
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print("Usage: {} db_file".format(sys.argv[0]))
|
print "Usage: {} db_file".format(sys.argv[0])
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
db_file = sys.argv[1]
|
||||||
@@ -14,4 +14,4 @@ db_file = sys.argv[1]
|
|||||||
parser = WeChatDBParser(db_file)
|
parser = WeChatDBParser(db_file)
|
||||||
chats = parser.msgs_by_chat.keys()
|
chats = parser.msgs_by_chat.keys()
|
||||||
for k in chats:
|
for k in chats:
|
||||||
print(parser.contacts[k], '\t', k)
|
print k.encode('utf-8'), '\t', parser.contacts_rev[k].encode('utf-8')
|
||||||
|
|||||||
@@ -1,46 +1,49 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: plot-num-msg-by-time.py
|
||||||
|
# Date: Wed Mar 25 17:44:39 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
from wechat.parser import WeChatDBParser
|
from wechat.parser import WeChatDBParser
|
||||||
|
from common.textutil import ensure_unicode
|
||||||
|
|
||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import sys
|
import sys, os
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
sys.exit("Usage: {0} <path to decrypted_database.db> <name>".format(sys.argv[0]))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
db_file = sys.argv[1]
|
||||||
if len(sys.argv) != 3:
|
name = ensure_unicode(sys.argv[2])
|
||||||
sys.exit("Usage: {0} <path to decoded_database.db> <name>".format(sys.argv[0]))
|
every_k_days = 2
|
||||||
|
|
||||||
db_file = sys.argv[1]
|
parser = WeChatDBParser(db_file)
|
||||||
name = sys.argv[2]
|
msgs = parser.msgs_by_chat[name]
|
||||||
every_k_days = 2
|
times = [x.createTime for x in msgs]
|
||||||
|
start_time = times[0]
|
||||||
|
diffs = [(x - start_time).days for x in times]
|
||||||
|
max_day = diffs[-1]
|
||||||
|
|
||||||
parser = WeChatDBParser(db_file)
|
width = 20
|
||||||
msgs = parser.msgs_by_chat[name]
|
numbers = range((max_day / width + 1) * width + 1)[::width]
|
||||||
times = [x.createTime for x in msgs]
|
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
|
||||||
start_time = times[0]
|
plt.xticks(numbers, labels)
|
||||||
diffs = [(x - start_time).days for x in times]
|
plt.xlabel("Date")
|
||||||
max_day = diffs[-1]
|
plt.ylabel("Number of msgs in k days")
|
||||||
|
plt.hist(diffs, bins=max_day / every_k_days)
|
||||||
width = 20
|
plt.show()
|
||||||
numbers = range((max_day / width + 1) * width + 1)[::width]
|
|
||||||
labels = [(start_time + timedelta(x)).strftime("%m/%d") for x in numbers]
|
|
||||||
plt.xticks(numbers, labels)
|
|
||||||
plt.xlabel("Date")
|
|
||||||
plt.ylabel("Number of msgs in k days")
|
|
||||||
plt.hist(diffs, bins=max_day / every_k_days)
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
# statistics by hour
|
# statistics by hour
|
||||||
# I'm in a different time zone in this period:
|
# I'm in a different time zone in this period:
|
||||||
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
|
#TZ_DELTA = {(datetime(2014, 7, 13), datetime(2014, 10, 1)): -15}
|
||||||
#def real_hour(x):
|
#def real_hour(x):
|
||||||
#for k, v in TZ_DELTA.items():
|
#for k, v in TZ_DELTA.iteritems():
|
||||||
#if x > k[0] and x < k[1]:
|
#if x > k[0] and x < k[1]:
|
||||||
#print x
|
#print x
|
||||||
#return (x.hour + v + 24) % 24
|
#return (x.hour + v + 24) % 24
|
||||||
#return x.hour
|
#return x.hour
|
||||||
#hours = [real_hour(x) for x in times]
|
#hours = [real_hour(x) for x in times]
|
||||||
#plt.ylabel("Number of msgs")
|
#plt.ylabel("Number of msgs")
|
||||||
#plt.xlabel("Hour in a day")
|
#plt.xlabel("Hour in a day")
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
pyquery
|
|
||||||
javaobj-py3>=0.4.1
|
|
||||||
pycryptodome
|
|
||||||
pillow>=7.0
|
|
||||||
requests
|
|
||||||
pysqlcipher3>=1.0.3
|
|
||||||
csscompressor
|
|
||||||
numpy
|
|
||||||
ipython
|
|
||||||
websocket-client
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
# File: compile_silk.sh
|
||||||
|
# Date: Tue Jun 16 22:26:49 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
cd `dirname "$0"`/silk
|
cd `dirname "$0"`/silk
|
||||||
make
|
make
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ ifeq (yes,$(USE_NEON))
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
CFLAGS += -Wall -O3
|
CFLAGS += -Wall -enable-threads -O3
|
||||||
|
|
||||||
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
|
||||||
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
|
CFLAGS += $(call cppflags-from-defines,$(ADDED_DEFINES))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -21,10 +21,10 @@ def set_level_color(lvl, color):
|
|||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
set_level_color(logging.INFO, '\033[1;32m')
|
set_level_color(logging.INFO, '\033[1;32m')
|
||||||
set_level_color(logging.WARN, '\033[1;33m')
|
set_level_color(logging.WARN, '\033[1;31m')
|
||||||
set_level_color(logging.ERROR, '\033[1;31m')
|
set_level_color(logging.ERROR, '\033[1;31m')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logger.info("info")
|
logger.info("info")
|
||||||
logger.warning("warn")
|
logger.warn("warn")
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +1,78 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: audio.py
|
||||||
|
# Date: Fri Jun 26 10:42:41 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import tempfile
|
from subprocess import PIPE, Popen, call
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from .common.textutil import get_file_b64
|
import pysox
|
||||||
from .common.procutil import subproc_succ
|
|
||||||
|
from common.textutil import get_file_b64
|
||||||
|
|
||||||
SILK_DECODER = os.path.join(os.path.dirname(__file__),
|
SILK_DECODER = os.path.join(os.path.dirname(__file__),
|
||||||
'../third-party/silk/decoder')
|
'../third-party/silk/decoder')
|
||||||
|
if not os.path.exists(SILK_DECODER):
|
||||||
|
logger.error("Silk decoder is not compiled. Please see README.md.")
|
||||||
|
raise RuntimeError()
|
||||||
|
|
||||||
def parse_wechat_audio_file(file_name):
|
def parse_wechat_audio_file(file_name):
|
||||||
try:
|
try:
|
||||||
return do_parse_wechat_audio_file(file_name)
|
return do_parse_wechat_audio_file(file_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error when parsing audio file {file_name}: {str(e)}")
|
logger.error("Pase audio file {} error!".format(file_name))
|
||||||
|
logger.error(e)
|
||||||
return "", 0
|
return "", 0
|
||||||
|
|
||||||
def do_parse_wechat_audio_file(file_name):
|
def do_parse_wechat_audio_file(file_name):
|
||||||
""" return a mp3 stored in base64 unicode string, and the duration"""
|
""" return a mp3 base64 string, and the duration"""
|
||||||
if not file_name: return "", 0
|
if not file_name: return "", 0
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="wechatdump_audio") as temp:
|
mp3_file = os.path.join('/tmp',
|
||||||
mp3_file = os.path.join(temp,
|
os.path.basename(file_name)[:-4] + '.mp3')
|
||||||
os.path.basename(file_name)[:-4] + '.mp3')
|
with open(file_name) as f:
|
||||||
with open(file_name, 'rb') as f:
|
header = f.read(10)
|
||||||
header = f.read(10)
|
if 'AMR' in header:
|
||||||
if b'AMR' in header:
|
# maybe this is faster than calling sox from command line?
|
||||||
cmd = f"sox -e signed -c 1 {file_name} {mp3_file}"
|
infile = pysox.CSoxStream(file_name)
|
||||||
subproc_succ(cmd)
|
outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
||||||
cmd = f"soxi -D {mp3_file}"
|
chain = pysox.CEffectsChain(infile, outfile)
|
||||||
duration = float(subproc_succ(cmd))
|
chain.flow_effects()
|
||||||
|
outfile.close()
|
||||||
|
|
||||||
# The below is python2 only. It should be equivalent to using sox from command line
|
signal = infile.get_signal().get_signalinfo()
|
||||||
# import pysox
|
duration = signal['length'] * 1.0 / signal['rate']
|
||||||
# infile = pysox.CSoxStream(file_name)
|
elif 'SILK' in header:
|
||||||
# outfile = pysox.CSoxStream(mp3_file, 'w', infile.get_signal())
|
raw_file = os.path.join('/tmp',
|
||||||
# chain = pysox.CEffectsChain(infile, outfile)
|
os.path.basename(file_name)[:-4] + '.raw')
|
||||||
# chain.flow_effects()
|
proc = Popen('{0} {1} {2}'.format(SILK_DECODER,
|
||||||
# outfile.close()
|
file_name, raw_file),
|
||||||
# signal = infile.get_signal().get_signalinfo()
|
shell=True, stdout=PIPE, stderr=PIPE)
|
||||||
# duration = signal['length'] * 1.0 / signal['rate']
|
stdout = proc.communicate()[0]
|
||||||
elif b'SILK' in header:
|
for line in stdout.split('\n'):
|
||||||
if not os.path.exists(SILK_DECODER):
|
if 'File length' in line:
|
||||||
raise RuntimeError("Silk decoder is not compiled. Please see README.md.")
|
duration = float(line[13:-3].strip())
|
||||||
|
break
|
||||||
raw_file = os.path.join(temp,
|
|
||||||
os.path.basename(file_name)[:-4] + '.raw')
|
|
||||||
cmd = '{0} {1} {2}'.format(SILK_DECODER, file_name, raw_file)
|
|
||||||
out = subproc_succ(cmd)
|
|
||||||
for line in out.split(b'\n'):
|
|
||||||
if b'File length' in line:
|
|
||||||
duration = float(line[13:-3].strip())
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise RuntimeError("Error decoding silk audio file!" + out.decode('utf-8'))
|
|
||||||
|
|
||||||
# TODO don't know how to do this with python
|
|
||||||
subproc_succ('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(raw_file, mp3_file))
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Audio file format cannot be recognized.")
|
raise RuntimeError("Error decoding silk audio file!")
|
||||||
|
|
||||||
|
# I don't know how to do this with pysox
|
||||||
|
proc = call('sox -r 24000 -e signed -b 16 -c 1 {} {}'.format(
|
||||||
|
raw_file, mp3_file), shell=True)
|
||||||
|
os.unlink(raw_file)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Unsupported Audio Format! This is a bug!")
|
||||||
|
try:
|
||||||
mp3_string = get_file_b64(mp3_file)
|
mp3_string = get_file_b64(mp3_file)
|
||||||
|
os.unlink(mp3_file)
|
||||||
|
except:
|
||||||
|
raise RuntimeError("Failed to decode audio file: {}".format(file_name))
|
||||||
return mp3_string, duration
|
return mp3_string, duration
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
fname = sys.argv[1]
|
fname = sys.argv[1]
|
||||||
print(parse_wechat_audio_file(fname)[1])
|
print parse_wechat_audio_file(fname)[1]
|
||||||
|
|||||||
@@ -1,137 +1,71 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python2
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
|
# File: avatar.py
|
||||||
|
# Date: Thu Jun 18 00:02:07 2015 +0800
|
||||||
|
# Author: Yuxin Wu <[email protected]>
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import cStringIO
|
||||||
import glob
|
|
||||||
import os
|
import os
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from .common.textutil import md5
|
from common.textutil import ensure_bin_str, md5
|
||||||
|
|
||||||
|
|
||||||
def _filename_priority(s):
|
|
||||||
if "_hd" in s and s.endswith(".png"):
|
|
||||||
return 10
|
|
||||||
else:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
class AvatarReader(object):
|
class AvatarReader(object):
|
||||||
def __init__(self, res_dir, avt_db="avatar.index"):
|
def __init__(self, avt_dir, avt_db="avatar.index"):
|
||||||
self.sfs_dir = os.path.join(res_dir, 'sfs')
|
self.avt_dir = avt_dir
|
||||||
|
|
||||||
# new location of avatar, see #50
|
|
||||||
self.avt_dir = os.path.join(res_dir, 'avatar')
|
|
||||||
if not os.path.isdir(self.avt_dir) or len(os.listdir(self.avt_dir)) == 0:
|
|
||||||
self.avt_dir = None
|
|
||||||
|
|
||||||
self.avt_db = avt_db
|
self.avt_db = avt_db
|
||||||
self._use_avt = True
|
if self.avt_db is None or not os.path.isfile(self.avt_db):
|
||||||
if self.avt_db is not None:
|
logger.warn(
|
||||||
if len(glob.glob(os.path.join(self.sfs_dir, 'avatar*'))) == 0:
|
"Avatar database {} not found. Will not use avatar!".format(avt_db))
|
||||||
# has sfs/avatar*
|
self.avt_db = None
|
||||||
self.avt_db = None
|
|
||||||
if self.avt_dir is None and self.avt_db is None:
|
|
||||||
logger.warn("Cannot find avatar storage. Will not use avatar!")
|
|
||||||
self._use_avt = False
|
|
||||||
|
|
||||||
def get_avatar_from_avtdb(self, avtid):
|
|
||||||
try:
|
|
||||||
candidates = self._search_avt_db(avtid)
|
|
||||||
candidates = sorted(candidates, key=lambda x: _filename_priority(x[0]), reverse=True)
|
|
||||||
for c in candidates:
|
|
||||||
path, offset, size = c
|
|
||||||
return self.read_img_from_block(path, offset, size)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_avatar_from_avtdir(self, avtid) -> Image.Image | None:
|
|
||||||
dir1, dir2 = avtid[:2], avtid[2:4]
|
|
||||||
candidates = glob.glob(os.path.join(self.avt_dir, dir1, dir2, f"*{avtid}*"))
|
|
||||||
candidates = sorted(set(candidates), key=_filename_priority, reverse=True)
|
|
||||||
for cand in candidates:
|
|
||||||
if os.path.isdir(cand):
|
|
||||||
candidates.extend(os.path.join(cand, x) for x in os.listdir(cand))
|
|
||||||
for cand in candidates:
|
|
||||||
if os.path.isdir(cand):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if cand.endswith(".bm"):
|
|
||||||
return self.read_bm_file(cand)
|
|
||||||
else:
|
|
||||||
return Image.open(cand)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("")
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_avatar(self, username):
|
def get_avatar(self, username):
|
||||||
""" username: `username` field in db.rcontact"""
|
""" username: `username` field in db.rcontact"""
|
||||||
if not self._use_avt:
|
if self.avt_db is None: return None
|
||||||
|
|
||||||
|
username = ensure_bin_str(username)
|
||||||
|
filename = md5(username)
|
||||||
|
dir1, dir2 = filename[:2], filename[2:4]
|
||||||
|
filename = os.path.join(dir1, dir2,
|
||||||
|
"user_{}.png".format(filename))
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
pos, size = self.query_index(filename)
|
||||||
|
return self.read_img(pos, size)
|
||||||
|
except TypeError:
|
||||||
|
logger.warn("Avatar for {} not found in avatar database.".format(username))
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
print e
|
||||||
|
logger.warn("Failed to retrieve avatar!")
|
||||||
return None
|
return None
|
||||||
avtid = md5(username.encode('utf-8'))
|
|
||||||
|
|
||||||
if self.avt_db is not None:
|
|
||||||
ret = self.get_avatar_from_avtdb(avtid)
|
|
||||||
if ret is not None:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
if self.avt_dir is not None:
|
def read_img(self, pos, size):
|
||||||
ret = self.get_avatar_from_avtdir(avtid)
|
|
||||||
if ret is not None:
|
|
||||||
return ret
|
|
||||||
logger.warning("Avatar file for {} not found.".format(username))
|
|
||||||
|
|
||||||
def save_avatar_to_avtdir(self, username: str, im: Image.Image):
|
|
||||||
"""Save a downloaded avatar to avtdir so it can be reused next time."""
|
|
||||||
avtid = md5(username.encode('utf-8'))
|
|
||||||
dir1, dir2 = avtid[:2], avtid[2:4]
|
|
||||||
fname = os.path.join(self.avt_dir, dir1, dir2, f"user_{avtid}.png")
|
|
||||||
os.makedirs(os.path.dirname(fname), exist_ok=True)
|
|
||||||
logger.info(f"Caching downloaded avatar for {username} to {fname}.")
|
|
||||||
im.save(fname, 'PNG')
|
|
||||||
|
|
||||||
def read_img_from_block(self, filename, pos, size):
|
|
||||||
file_idx = pos >> 32
|
file_idx = pos >> 32
|
||||||
fname = os.path.join(self.sfs_dir,
|
fname = os.path.join(self.avt_dir,
|
||||||
'avatar.block.' + '{:05d}'.format(file_idx))
|
'avatar.block.' + '{:05d}'.format(file_idx))
|
||||||
# offset of each block file: 17 + len(path)
|
# a 64-byte offset of each block file
|
||||||
start_pos = pos - file_idx * (2**32) + 16 + len(filename) + 1
|
start_pos = pos - file_idx * (2**32) + 64
|
||||||
try:
|
try:
|
||||||
with open(fname, 'rb') as f:
|
with open(fname, 'rb') as f:
|
||||||
f.seek(start_pos)
|
f.seek(start_pos)
|
||||||
data = f.read(size)
|
data = f.read(size)
|
||||||
im = Image.open(io.BytesIO(data))
|
im = Image.open(cStringIO.StringIO(data))
|
||||||
return im
|
return im
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
|
logger.warn("Cannot read avatar from {}: {}".format(fname, str(e)))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def read_bm_file(self, fname):
|
def query_index(self, filename):
|
||||||
# history at https://github.com/ppwwyyxx/wechat-dump/pull/14
|
|
||||||
with open(fname, 'rb') as f:
|
|
||||||
# filesize is 36880=96x96x4+16
|
|
||||||
size = (96, 96, 3)
|
|
||||||
img = np.zeros(size, dtype='uint8')
|
|
||||||
for i in range(96):
|
|
||||||
for j in range(96):
|
|
||||||
r, g, b, a = f.read(4)
|
|
||||||
img[i,j] = (r, g, b)
|
|
||||||
return Image.fromarray(img, mode="RGB")
|
|
||||||
|
|
||||||
def _search_avt_db(self, avtid):
|
|
||||||
conn = sqlite3.connect(self.avt_db)
|
conn = sqlite3.connect(self.avt_db)
|
||||||
cursor = conn.execute("select FileName,Offset,Size from Index_avatar")
|
cursor = conn.execute("select Offset,Size from Index_avatar where FileName='{}'".format(filename))
|
||||||
candidates = []
|
pos, size = cursor.fetchone()
|
||||||
for path, offset, size in cursor:
|
return pos, size
|
||||||
if avtid in path:
|
|
||||||
candidates.append((path, offset, size))
|
|
||||||
return candidates
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
r = AvatarReader(sys.argv[1], sys.argv[2])
|
|
||||||
print(r.get_avatar(sys.argv[3]))
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import io
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
try:
|
|
||||||
import imghdr as _imghdr
|
|
||||||
except ModuleNotFoundError: # Python 3.13+
|
|
||||||
_imghdr = None
|
|
||||||
|
|
||||||
|
|
||||||
def what(file: str | Path | None = None, h: bytes | None = None) -> str | None:
|
|
||||||
"""
|
|
||||||
Compatibility wrapper for the removed stdlib `imghdr` module (Python 3.13+).
|
|
||||||
|
|
||||||
Behaves like `imghdr.what(file, h)` and returns a lowercase format string
|
|
||||||
like "jpeg", "png", "gif", or None if unknown.
|
|
||||||
"""
|
|
||||||
if _imghdr is not None:
|
|
||||||
return _imghdr.what(file, h)
|
|
||||||
if h is not None:
|
|
||||||
return _what_from_bytes(h)
|
|
||||||
if file is None:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
with Image.open(str(file)) as im:
|
|
||||||
fmt = im.format
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return fmt.lower() if fmt else None
|
|
||||||
|
|
||||||
|
|
||||||
def _what_from_bytes(data: bytes) -> str | None:
|
|
||||||
if data.startswith(b"\xff\xd8\xff"):
|
|
||||||
return "jpeg"
|
|
||||||
if data.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
||||||
return "png"
|
|
||||||
if data[:6] in (b"GIF87a", b"GIF89a"):
|
|
||||||
return "gif"
|
|
||||||
if data.startswith(b"BM"):
|
|
||||||
return "bmp"
|
|
||||||
if len(data) >= 12 and data.startswith(b"RIFF") and data[8:12] == b"WEBP":
|
|
||||||
return "webp"
|
|
||||||
if data.startswith(b"II*\x00") or data.startswith(b"MM\x00*"):
|
|
||||||
return "tiff"
|
|
||||||
try:
|
|
||||||
with Image.open(io.BytesIO(data)) as im:
|
|
||||||
fmt = im.format
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return fmt.lower() if fmt else None
|
|
||||||