Initial release v1.0.0 - Messages Mod+ KernelSU-Next module

This commit is contained in:
sssnake
2026-04-03 07:12:16 -07:00
commit 0953b1d929
9 changed files with 1758 additions and 0 deletions

33
LICENSE Normal file
View File

@@ -0,0 +1,33 @@
Messages Mod+ License
Copyright (c) 2026 SetecLabs
Permission is granted, free of charge, to any person obtaining a copy of
this software and associated files (the "Software"), to use the Software
for personal, non-commercial purposes only.
The following restrictions apply:
1. You may NOT redistribute, share, or otherwise distribute the Software
or any portion of it without prior written approval from the copyright
holder.
2. You may NOT modify, adapt, or create derivative works based on the
Software without prior written approval from the copyright holder.
3. You may NOT use the Software for any commercial purpose, or incorporate
it into any product or service offered to others, without prior written
approval from the copyright holder.
4. This license notice must be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
For licensing inquiries or approval requests, contact: SetecLabs

91
README.md Normal file
View File

@@ -0,0 +1,91 @@
# Messages Mod+
A KernelSU-Next module that fixes RCS (Rich Communication Services) on rooted Android devices by reinstalling Google Messages as a user-space app.
## The Problem
On rooted devices (KernelSU, Magisk, etc.), Google Messages installed as a system app can experience persistent RCS failures — even when Play Integrity passes at the STRONG level. The system-level installation hooks into the telephony stack differently than a user-space app, and this can cause RCS provisioning and registration to silently fail or intermittently break.
## How It Works
The module disables the system copy of Google Messages and reinstalls it as a user-space update. Android treats it as an `UPDATED_SYSTEM_APP` — the system base remains dormant while the active copy lives in `/data/app/` and behaves like a regular user-installed application. This difference in how the app is loaded and permissioned resolves the RCS issues.
The original system APK stays untouched in `/product/priv-app/` — it only becomes active again if you explicitly revert.
## Features
### KernelSU Module
- **Action Button**: One-tap apply — disables system Messages, reinstalls as user-space
- **WebUI Dashboard** with:
- Live status display (MODDED / SYSTEM / NOT INSTALLED)
- SMS/MMS database backup & restore
- RCS database backup & restore
- Message attachments backup (photos, videos, media)
- Per-backup restore and delete
- Revert to stock with one tap
- **Auto-revert** on module uninstall
- All backups stored at `/sdcard/MessagesModPlus/`
### Standalone Script
- Runs from a computer via ADB
- Auto-detect or manual package configuration
- Interactive menu with colored output
- Same full functionality as the module
## Installation
### KernelSU-Next Module
1. Download `MessagesModPlus-v1.0.0.zip` from Releases
2. Open KernelSU Manager
3. Go to Modules > Install from storage
4. Select the zip file
5. Open the module's WebUI or press the Action button
### Standalone Script (via ADB)
```bash
chmod +x messages_mod_standalone.sh
./messages_mod_standalone.sh
```
Requirements:
- ADB connected to a rooted device
- Root shell access (KernelSU, Magisk, etc.)
## After Applying
1. Open Google Messages on your device
2. Set it as the default SMS app if prompted
3. Grant all requested permissions
4. RCS should provision normally
## Reverting
Three ways to revert:
- **WebUI**: Tap "Revert to Stock"
- **KernelSU Manager**: Remove the module (auto-reverts via uninstall.sh)
- **Standalone**: Select option 3 from the menu
## File Structure
```
MessagesModPlus/
├── module.prop # Module metadata
├── customize.sh # Installer
├── action.sh # Action button - runs apply
├── uninstall.sh # Auto-revert on module removal
├── scripts/
│ └── messages_mod.sh # Core operations (all commands)
├── webroot/
│ └── index.html # WebUI dashboard
└── messages_mod_standalone.sh # Standalone ADB script
```
## Compatibility
- Tested on Pixel devices with KernelSU-Next
- Should work with any rooted Android device running Google Messages
- Requires Android 10+ (API 29+)
## License
Free for personal use. Redistribution, modification, or use beyond personal purposes requires approval. See LICENSE for full terms.

14
action.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/system/bin/sh
# Messages Mod+ - Action Button Handler
# Runs the main mod: disable system Messages, reinstall as user-space
MODDIR="${0%/*}"
SCRIPT="${MODDIR}/scripts/messages_mod.sh"
if [ ! -f "$SCRIPT" ]; then
echo "Error: messages_mod.sh not found"
exit 1
fi
export MODDIR
sh "$SCRIPT" apply

35
customize.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/system/bin/sh
# Messages Mod+ - Module Installer
SKIPUNZIP=1
ui_print " "
ui_print " ============================="
ui_print " Messages Mod+"
ui_print " Fix RCS on Rooted Devices"
ui_print " ============================="
ui_print " "
# Extract module files
ui_print "- Extracting module files..."
unzip -o "$ZIPFILE" -x 'META-INF/*' -d "$MODPATH" >&2
# Set permissions
ui_print "- Setting permissions..."
set_perm_recursive "$MODPATH" 0 0 0755 0644
set_perm "$MODPATH/scripts/messages_mod.sh" 0 0 0755
set_perm "$MODPATH/action.sh" 0 0 0755
# Create backup directory on internal storage
mkdir -p /sdcard/MessagesModPlus/sms
mkdir -p /sdcard/MessagesModPlus/rcs
mkdir -p /sdcard/MessagesModPlus/attachments
mkdir -p /sdcard/MessagesModPlus/apks
ui_print " "
ui_print "- Backup directory: /sdcard/MessagesModPlus/"
ui_print " "
ui_print "- Installation complete!"
ui_print "- Open the module's WebUI to apply the mod"
ui_print "- Or press the Action button for quick apply"
ui_print " "

366
messages_mod_standalone.sh Executable file
View File

@@ -0,0 +1,366 @@
#!/usr/bin/env bash
# ============================================================
# Messages Mod+ Standalone Script
# Fix RCS on rooted Android devices by reinstalling
# Google Messages as a user-space app
#
# Run from a computer with ADB connected to a rooted device
# ============================================================
set -euo pipefail
# ---- USER CONFIGURABLE VARIABLES ----
# Set these manually, or leave empty for auto-detection
MSG_PKG="" # e.g. "com.google.android.apps.messaging"
BACKUP_DIR="" # e.g. "./messages_backup"
# ---- END USER CONFIG ----
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
log() { echo -e "${BLUE}[*]${NC} $1"; }
ok() { echo -e "${GREEN}[+]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
err() { echo -e "${RED}[-]${NC} $1"; }
hdr() { echo -e "\n${BOLD}${CYAN}=== $1 ===${NC}\n"; }
su_cmd() {
adb shell su -c "$1" 2>&1
}
check_adb() {
if ! command -v adb &>/dev/null; then
err "ADB not found in PATH"
exit 1
fi
local devices
devices=$(adb devices | grep -w "device" | grep -v "List")
if [ -z "$devices" ]; then
err "No ADB device connected"
exit 1
fi
local serial
serial=$(echo "$devices" | head -1 | awk '{print $1}')
ok "Device connected: ${serial}"
}
check_root() {
local whoami
whoami=$(su_cmd 'id -u' | tr -d '[:space:]')
if [ "$whoami" != "0" ]; then
err "Root shell not available (su failed)"
err "Make sure KernelSU, Magisk, or another root solution is active"
exit 1
fi
ok "Root shell verified"
}
auto_detect() {
log "Auto-detecting Google Messages package..."
# Try the standard package name
local found
found=$(adb shell pm list packages 2>/dev/null | grep "com.google.android.apps.messaging" || true)
if [ -n "$found" ]; then
MSG_PKG="com.google.android.apps.messaging"
ok "Found: ${MSG_PKG}"
else
# Broader search
warn "Standard package not found, searching..."
found=$(adb shell pm list packages 2>/dev/null | grep -i "messaging\|messages" || true)
if [ -n "$found" ]; then
echo ""
echo "Found messaging packages:"
echo "$found" | sed 's/package:/ /' | nl
echo ""
read -rp "Enter the number of the correct package (or type it manually): " choice
if [[ "$choice" =~ ^[0-9]+$ ]]; then
MSG_PKG=$(echo "$found" | sed 's/package://' | sed -n "${choice}p" | tr -d '[:space:]')
else
MSG_PKG="$choice"
fi
else
err "No messaging packages found on device"
exit 1
fi
fi
BACKUP_DIR="${BACKUP_DIR:-./messages_backup_$(date +%Y%m%d)}"
}
manual_config() {
echo ""
read -rp "Enter Messages package name [com.google.android.apps.messaging]: " input_pkg
MSG_PKG="${input_pkg:-com.google.android.apps.messaging}"
read -rp "Enter local backup directory [./messages_backup]: " input_dir
BACKUP_DIR="${input_dir:-./messages_backup}"
# Verify package exists
if ! adb shell pm list packages | grep -q "$MSG_PKG"; then
err "Package '${MSG_PKG}' not found on device"
exit 1
fi
ok "Package verified: ${MSG_PKG}"
}
backup_databases() {
hdr "Backing Up Databases"
mkdir -p "${BACKUP_DIR}/databases"
# SMS/MMS
local sms_db="/data/data/com.android.providers.telephony/databases/mmssms.db"
log "Backing up SMS/MMS database..."
adb shell su -c "cat '${sms_db}'" > "${BACKUP_DIR}/databases/mmssms.db" 2>/dev/null && \
ok "SMS/MMS: $(du -h "${BACKUP_DIR}/databases/mmssms.db" | cut -f1)" || \
warn "SMS/MMS database not found or empty"
# RCS / Bugle
local rcs_db="/data/data/${MSG_PKG}/databases/bugle_db"
log "Backing up RCS database..."
adb shell su -c "cat '${rcs_db}'" > "${BACKUP_DIR}/databases/bugle_db.db" 2>/dev/null && \
ok "RCS: $(du -h "${BACKUP_DIR}/databases/bugle_db.db" | cut -f1)" || \
warn "RCS database not found or empty"
# Remove zero-byte files from failed backups
find "${BACKUP_DIR}/databases" -size 0 -delete 2>/dev/null || true
}
pull_apks() {
hdr "Pulling APK Splits"
mkdir -p "${BACKUP_DIR}/apks"
local apk_paths
apk_paths=$(adb shell su -c "pm path '${MSG_PKG}'" | sed 's/package://' | tr -d '\r')
if [ -z "$apk_paths" ]; then
err "Could not find APK paths for ${MSG_PKG}"
exit 1
fi
local count=0
while IFS= read -r apk_path; do
apk_path=$(echo "$apk_path" | tr -d '[:space:]')
[ -z "$apk_path" ] && continue
local apk_name
apk_name=$(basename "$apk_path")
log "Pulling ${apk_name}..."
adb shell su -c "cat '${apk_path}'" > "${BACKUP_DIR}/apks/${apk_name}"
local size
size=$(du -h "${BACKUP_DIR}/apks/${apk_name}" | cut -f1)
ok " ${apk_name} (${size})"
count=$((count + 1))
done <<< "$apk_paths"
ok "Pulled ${count} APK split(s)"
}
disable_system() {
hdr "Disabling System Messages"
log "Disabling ${MSG_PKG}..."
local result
result=$(adb shell su -c "pm disable-user --user 0 '${MSG_PKG}'" 2>&1)
echo " ${result}"
if echo "$result" | grep -q "disabled"; then
ok "System app disabled"
else
warn "Disable command returned unexpected output"
fi
}
reinstall_userspace() {
hdr "Reinstalling as User-Space App"
local apk_files
apk_files=$(ls -1 "${BACKUP_DIR}/apks/"*.apk 2>/dev/null)
local apk_count
apk_count=$(echo "$apk_files" | wc -l)
if [ -z "$apk_files" ] || [ "$apk_count" -eq 0 ]; then
err "No APK files found in ${BACKUP_DIR}/apks/"
exit 1
fi
log "Installing ${apk_count} APK split(s)..."
local install_args=""
while IFS= read -r apk; do
install_args="${install_args} ${apk}"
done <<< "$apk_files"
local result
if [ "$apk_count" -gt 1 ]; then
result=$(adb install-multiple $install_args 2>&1)
else
result=$(adb install $install_args 2>&1)
fi
echo " ${result}"
if echo "$result" | grep -qi "success"; then
ok "Installation successful"
else
err "Installation failed"
warn "Attempting to re-enable system app..."
adb shell pm enable "$MSG_PKG" 2>/dev/null
exit 1
fi
}
reenable_package() {
hdr "Re-enabling Package"
local result
result=$(adb shell pm enable "$MSG_PKG" 2>&1)
echo " ${result}"
ok "Package enabled"
}
verify() {
hdr "Verification"
local flags
flags=$(adb shell su -c "dumpsys package '${MSG_PKG}'" | grep "pkgFlags" | head -1)
if echo "$flags" | grep -q "UPDATED_SYSTEM_APP"; then
ok "Messages is running as UPDATED_SYSTEM_APP (user-space)"
echo ""
echo -e "${GREEN}${BOLD} Messages Mod+ applied successfully!${NC}"
echo ""
echo " Next steps on your device:"
echo " 1. Open Google Messages"
echo " 2. Set as default SMS app if prompted"
echo " 3. Grant permissions"
echo " 4. RCS should provision normally"
echo ""
else
warn "Package flags: ${flags}"
warn "State may not be as expected - verify on device"
fi
}
revert_to_stock() {
hdr "Reverting to Stock"
log "Removing user-space updates..."
adb shell pm uninstall-updates "$MSG_PKG" 2>&1 || true
adb shell pm enable "$MSG_PKG" 2>/dev/null
local flags
flags=$(adb shell su -c "dumpsys package '${MSG_PKG}'" | grep "pkgFlags" | head -1)
if echo "$flags" | grep -q "UPDATED_SYSTEM_APP"; then
warn "Revert may not have completed fully"
else
ok "Messages reverted to stock system app"
fi
}
show_menu() {
echo ""
echo -e "${BOLD}${CYAN}Messages Mod+${NC}"
echo -e "Fix RCS on rooted Android devices"
echo ""
echo " 1) Apply mod (full process)"
echo " 2) Backup databases only"
echo " 3) Revert to stock"
echo " 4) Check status"
echo " 5) Exit"
echo ""
read -rp "Select option [1-5]: " choice
echo ""
return "$choice"
}
show_status() {
hdr "Current Status"
local flags
flags=$(adb shell su -c "dumpsys package '${MSG_PKG}'" 2>/dev/null | grep "pkgFlags" | head -1 || echo "not found")
local code_path
code_path=$(adb shell su -c "dumpsys package '${MSG_PKG}'" 2>/dev/null | grep "codePath=" | head -1 | sed 's/.*codePath=//' || echo "unknown")
local enabled
enabled=$(adb shell pm list packages -e 2>/dev/null | grep -q "$MSG_PKG" && echo "Yes" || echo "No")
echo " Package: ${MSG_PKG}"
echo " Path: ${code_path}"
echo " Enabled: ${enabled}"
echo " Flags: ${flags}"
if echo "$flags" | grep -q "UPDATED_SYSTEM_APP"; then
ok "State: MODDED (user-space)"
elif echo "$flags" | grep -q "SYSTEM"; then
warn "State: SYSTEM (stock)"
else
log "State: Unknown"
fi
}
# ============================================================
# Main
# ============================================================
echo ""
echo -e "${BOLD}${CYAN}╔══════════════════════════════════╗${NC}"
echo -e "${BOLD}${CYAN}║ Messages Mod+ v1.0.0 ║${NC}"
echo -e "${BOLD}${CYAN}║ Fix RCS on Rooted Devices ║${NC}"
echo -e "${BOLD}${CYAN}╚══════════════════════════════════╝${NC}"
echo ""
# Preflight
check_adb
check_root
# Detection mode
echo ""
echo "Configuration mode:"
echo " 1) Auto-detect (recommended)"
echo " 2) Manual"
echo ""
read -rp "Select [1/2]: " mode
case "$mode" in
2) manual_config ;;
*) auto_detect ;;
esac
BACKUP_DIR="${BACKUP_DIR:-./messages_backup_$(date +%Y%m%d)}"
mkdir -p "$BACKUP_DIR"
log "Backup directory: ${BACKUP_DIR}"
# Main menu loop
while true; do
show_menu
opt=$?
case "$opt" in
1)
backup_databases
pull_apks
disable_system
reinstall_userspace
reenable_package
verify
;;
2)
backup_databases
;;
3)
revert_to_stock
;;
4)
show_status
;;
5)
echo "Bye!"
exit 0
;;
*)
warn "Invalid option"
;;
esac
done

6
module.prop Normal file
View File

@@ -0,0 +1,6 @@
id=messages_mod_plus
name=Messages Mod+
version=v1.0.0
versionCode=100
author=SetecLabs
description=Fix RCS on rooted devices by reinstalling Google Messages as a user-space app. Includes backup/restore for SMS, MMS, RCS databases and attachments.

485
scripts/messages_mod.sh Executable file
View File

@@ -0,0 +1,485 @@
#!/system/bin/sh
# Messages Mod+ - Main Operations Script
# Runs on-device with root privileges
# Usage: messages_mod.sh <command> [args]
MODDIR="${MODDIR:-$(dirname "$(dirname "$(readlink -f "$0")")")}"
BACKUP_DIR="/sdcard/MessagesModPlus"
MSG_PKG="com.google.android.apps.messaging"
TELEPHONY_PKG="com.android.providers.telephony"
CARRIER_PKG="com.google.android.ims"
SMS_DB="/data/data/${TELEPHONY_PKG}/databases/mmssms.db"
RCS_DB="/data/data/${MSG_PKG}/databases/bugle_db"
ATTACH_DIR="/data/data/${MSG_PKG}/files"
log() { echo "[MessagesModPlus] $1"; }
err() { echo "[MessagesModPlus:ERROR] $1" >&2; }
timestamp() { date +%Y%m%d_%H%M%S; }
ensure_backup_dir() {
mkdir -p "${BACKUP_DIR}/sms" "${BACKUP_DIR}/rcs" "${BACKUP_DIR}/attachments" "${BACKUP_DIR}/apks"
}
get_apk_paths() {
pm path "$MSG_PKG" 2>/dev/null | sed 's/^package://'
}
get_system_apk_path() {
dumpsys package "$MSG_PKG" | grep "codePath=" | grep -E "/system|/product|/system_ext|/vendor" | head -1 | sed 's/.*codePath=//' | tr -d ' '
}
is_system_app() {
dumpsys package "$MSG_PKG" | grep "pkgFlags" | head -1 | grep -q "SYSTEM"
}
is_updated_system_app() {
dumpsys package "$MSG_PKG" | grep "pkgFlags" | head -1 | grep -q "UPDATED_SYSTEM_APP"
}
is_package_enabled() {
pm list packages -e | grep -q "$MSG_PKG"
}
get_status() {
local status="unknown"
local system_path=""
local user_path=""
local enabled="false"
if ! pm list packages | grep -q "$MSG_PKG"; then
echo "NOT_INSTALLED|none|none|false"
return
fi
system_path=$(get_system_apk_path)
user_path=$(dumpsys package "$MSG_PKG" | grep "codePath=" | grep "/data/app" | head -1 | sed 's/.*codePath=//' | tr -d ' ')
if is_package_enabled; then
enabled="true"
fi
if is_updated_system_app; then
status="MODDED"
elif is_system_app; then
status="SYSTEM"
else
status="USER"
fi
echo "${status}|${system_path:-none}|${user_path:-none}|${enabled}"
}
cmd_status() {
local info
info=$(get_status)
local state=$(echo "$info" | cut -d'|' -f1)
local sys_path=$(echo "$info" | cut -d'|' -f2)
local usr_path=$(echo "$info" | cut -d'|' -f3)
local enabled=$(echo "$info" | cut -d'|' -f4)
echo "STATE=${state}"
echo "SYSTEM_PATH=${sys_path}"
echo "USER_PATH=${usr_path}"
echo "ENABLED=${enabled}"
# Backup info
local sms_count=0
local rcs_count=0
local attach_count=0
[ -d "${BACKUP_DIR}/sms" ] && sms_count=$(ls -1 "${BACKUP_DIR}/sms"/*.db 2>/dev/null | wc -l)
[ -d "${BACKUP_DIR}/rcs" ] && rcs_count=$(ls -1 "${BACKUP_DIR}/rcs"/*.db 2>/dev/null | wc -l)
[ -d "${BACKUP_DIR}/attachments" ] && attach_count=$(ls -1d "${BACKUP_DIR}/attachments"/*/ 2>/dev/null | wc -l)
echo "SMS_BACKUPS=${sms_count}"
echo "RCS_BACKUPS=${rcs_count}"
echo "ATTACH_BACKUPS=${attach_count}"
}
cmd_backup_sms() {
ensure_backup_dir
if [ ! -f "$SMS_DB" ]; then
err "SMS database not found at ${SMS_DB}"
return 1
fi
local ts=$(timestamp)
local dest="${BACKUP_DIR}/sms/mmssms_${ts}.db"
# Force a checkpoint so WAL data is flushed
cp "$SMS_DB" "$dest" 2>/dev/null
[ -f "${SMS_DB}-wal" ] && cp "${SMS_DB}-wal" "${dest}-wal" 2>/dev/null
[ -f "${SMS_DB}-journal" ] && cp "${SMS_DB}-journal" "${dest}-journal" 2>/dev/null
if [ -f "$dest" ]; then
local size=$(du -h "$dest" | cut -f1)
log "SMS backup saved: ${dest} (${size})"
echo "BACKUP=${dest}"
return 0
else
err "Failed to create SMS backup"
return 1
fi
}
cmd_backup_rcs() {
ensure_backup_dir
if [ ! -f "$RCS_DB" ]; then
err "RCS database not found at ${RCS_DB}"
return 1
fi
local ts=$(timestamp)
local dest="${BACKUP_DIR}/rcs/bugle_db_${ts}.db"
cp "$RCS_DB" "$dest" 2>/dev/null
[ -f "${RCS_DB}-wal" ] && cp "${RCS_DB}-wal" "${dest}-wal" 2>/dev/null
[ -f "${RCS_DB}-journal" ] && cp "${RCS_DB}-journal" "${dest}-journal" 2>/dev/null
if [ -f "$dest" ]; then
local size=$(du -h "$dest" | cut -f1)
log "RCS backup saved: ${dest} (${size})"
echo "BACKUP=${dest}"
return 0
else
err "Failed to create RCS backup"
return 1
fi
}
cmd_backup_attachments() {
ensure_backup_dir
if [ ! -d "$ATTACH_DIR" ]; then
err "Attachments directory not found at ${ATTACH_DIR}"
return 1
fi
local ts=$(timestamp)
local dest="${BACKUP_DIR}/attachments/${ts}"
mkdir -p "$dest"
# Copy all media/attachment files
local count=0
# bugle attachments
if [ -d "/data/data/${MSG_PKG}/files/photoEditor" ]; then
cp -r "/data/data/${MSG_PKG}/files/photoEditor" "$dest/" 2>/dev/null
fi
# Main media storage used by Messages
local media_dir="/data/user_de/0/${MSG_PKG}/files"
if [ -d "$media_dir" ]; then
cp -r "$media_dir" "$dest/user_de_files" 2>/dev/null
fi
# MMS parts (images, videos, audio from MMS)
local parts_dir="/data/data/${TELEPHONY_PKG}/app_parts"
if [ -d "$parts_dir" ]; then
cp -r "$parts_dir" "$dest/mms_parts" 2>/dev/null
fi
# Also grab from the standard media location
local bugle_media="/data/data/${MSG_PKG}/cache"
if [ -d "$bugle_media" ]; then
cp -r "$bugle_media" "$dest/cache" 2>/dev/null
fi
count=$(find "$dest" -type f 2>/dev/null | wc -l)
local size=$(du -sh "$dest" 2>/dev/null | cut -f1)
log "Attachments saved: ${dest} (${count} files, ${size})"
echo "BACKUP=${dest}"
echo "FILE_COUNT=${count}"
return 0
}
cmd_restore_sms() {
local backup_file="$1"
if [ -z "$backup_file" ]; then
# Use most recent backup
backup_file=$(ls -t "${BACKUP_DIR}/sms"/*.db 2>/dev/null | head -1)
fi
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
err "No SMS backup found"
return 1
fi
# Stop telephony provider temporarily
am force-stop "$TELEPHONY_PKG" 2>/dev/null
cp "$backup_file" "$SMS_DB"
[ -f "${backup_file}-wal" ] && cp "${backup_file}-wal" "${SMS_DB}-wal"
[ -f "${backup_file}-journal" ] && cp "${backup_file}-journal" "${SMS_DB}-journal"
# Fix ownership - telephony db is owned by radio
local radio_uid=$(stat -c '%u' /data/data/${TELEPHONY_PKG}/ 2>/dev/null || echo "1001")
local radio_gid=$(stat -c '%g' /data/data/${TELEPHONY_PKG}/ 2>/dev/null || echo "1001")
chown "${radio_uid}:${radio_gid}" "$SMS_DB" "${SMS_DB}-wal" "${SMS_DB}-journal" 2>/dev/null
chmod 660 "$SMS_DB" "${SMS_DB}-wal" "${SMS_DB}-journal" 2>/dev/null
restorecon "$SMS_DB" "${SMS_DB}-wal" "${SMS_DB}-journal" 2>/dev/null
log "SMS database restored from ${backup_file}"
log "Restart Messages app or reboot for changes to take effect"
echo "RESTORED=${backup_file}"
return 0
}
cmd_restore_rcs() {
local backup_file="$1"
if [ -z "$backup_file" ]; then
backup_file=$(ls -t "${BACKUP_DIR}/rcs"/*.db 2>/dev/null | head -1)
fi
if [ -z "$backup_file" ] || [ ! -f "$backup_file" ]; then
err "No RCS backup found"
return 1
fi
am force-stop "$MSG_PKG" 2>/dev/null
cp "$backup_file" "$RCS_DB"
[ -f "${backup_file}-wal" ] && cp "${backup_file}-wal" "${RCS_DB}-wal"
[ -f "${backup_file}-journal" ] && cp "${backup_file}-journal" "${RCS_DB}-journal"
# Fix ownership to match Messages app uid
local msg_uid=$(stat -c '%u' "/data/data/${MSG_PKG}/" 2>/dev/null)
local msg_gid=$(stat -c '%g' "/data/data/${MSG_PKG}/" 2>/dev/null)
if [ -n "$msg_uid" ]; then
chown "${msg_uid}:${msg_gid}" "$RCS_DB" "${RCS_DB}-wal" "${RCS_DB}-journal" 2>/dev/null
chmod 660 "$RCS_DB" "${RCS_DB}-wal" "${RCS_DB}-journal" 2>/dev/null
restorecon "$RCS_DB" "${RCS_DB}-wal" "${RCS_DB}-journal" 2>/dev/null
fi
log "RCS database restored from ${backup_file}"
echo "RESTORED=${backup_file}"
return 0
}
cmd_list_backups() {
local type="$1"
case "$type" in
sms)
ls -1t "${BACKUP_DIR}/sms"/*.db 2>/dev/null | while read f; do
local size=$(du -h "$f" | cut -f1)
local name=$(basename "$f")
echo "${f}|${name}|${size}"
done
;;
rcs)
ls -1t "${BACKUP_DIR}/rcs"/*.db 2>/dev/null | while read f; do
local size=$(du -h "$f" | cut -f1)
local name=$(basename "$f")
echo "${f}|${name}|${size}"
done
;;
attachments)
ls -1dt "${BACKUP_DIR}/attachments"/*/ 2>/dev/null | while read d; do
local size=$(du -sh "$d" 2>/dev/null | cut -f1)
local count=$(find "$d" -type f 2>/dev/null | wc -l)
local name=$(basename "$d")
echo "${d}|${name}|${size}|${count} files"
done
;;
esac
}
cmd_delete_backup() {
local path="$1"
# Safety: only allow deleting from our backup directory
case "$path" in
${BACKUP_DIR}/*)
if [ -f "$path" ]; then
rm -f "$path" "${path}-wal" "${path}-journal"
log "Deleted backup: ${path}"
elif [ -d "$path" ]; then
rm -rf "$path"
log "Deleted backup directory: ${path}"
else
err "Backup not found: ${path}"
return 1
fi
;;
*)
err "Refusing to delete outside backup directory: ${path}"
return 1
;;
esac
}
cmd_apply_mod() {
log "Starting Messages Mod+ apply..."
# Verify package exists
if ! pm list packages | grep -q "$MSG_PKG"; then
err "Google Messages not installed"
return 1
fi
# Check if already modded
if is_updated_system_app; then
log "Messages is already running as user-space updated system app"
echo "ALREADY_MODDED=true"
return 0
fi
# Step 1: Get current APK paths before disabling
log "Collecting APK paths..."
local apk_paths=$(get_apk_paths)
if [ -z "$apk_paths" ]; then
err "Could not find Messages APK paths"
return 1
fi
# Step 2: Save APKs to backup
ensure_backup_dir
local apk_dir="${BACKUP_DIR}/apks"
rm -f "${apk_dir}"/*.apk 2>/dev/null
local apk_count=0
local apk_list=""
echo "$apk_paths" | while read apk_path; do
if [ -f "$apk_path" ]; then
local apk_name=$(basename "$apk_path")
cp "$apk_path" "${apk_dir}/${apk_name}"
log "Saved: ${apk_name}"
fi
done
# Build the install command from saved APKs
apk_list=$(ls -1 "${apk_dir}"/*.apk 2>/dev/null | tr '\n' ' ')
if [ -z "$apk_list" ]; then
err "No APKs were saved, aborting"
return 1
fi
apk_count=$(ls -1 "${apk_dir}"/*.apk 2>/dev/null | wc -l)
log "Saved ${apk_count} APK split(s)"
# Step 3: Disable the system app
log "Disabling system Messages..."
pm disable-user --user 0 "$MSG_PKG" 2>&1
# Step 4: Reinstall as user-space app
log "Installing as user-space app..."
local install_result
# pm install requires -t for test and --bypass-low-target-sdk-block on newer Android
# Use session-based install for split APKs
local session_id=$(pm install-create 2>&1 | grep -oE '[0-9]+')
if [ -z "$session_id" ]; then
err "Failed to create install session"
pm enable "$MSG_PKG" 2>/dev/null
return 1
fi
local idx=0
for apk in ${apk_dir}/*.apk; do
local apk_size=$(stat -c '%s' "$apk" 2>/dev/null || wc -c < "$apk")
local apk_name=$(basename "$apk")
pm install-write -S "$apk_size" "$session_id" "$apk_name" "$apk" 2>&1
idx=$((idx + 1))
done
install_result=$(pm install-commit "$session_id" 2>&1)
if echo "$install_result" | grep -qi "success"; then
log "Installation successful"
else
# Fallback: try direct install
log "Session install returned: ${install_result}, trying direct method..."
local apk_args=""
for apk in ${apk_dir}/*.apk; do
apk_args="${apk_args} ${apk}"
done
install_result=$(pm install $apk_args 2>&1)
if ! echo "$install_result" | grep -qi "success"; then
err "Installation failed: ${install_result}"
pm enable "$MSG_PKG" 2>/dev/null
return 1
fi
fi
# Step 5: Re-enable
log "Re-enabling package..."
pm enable "$MSG_PKG" 2>&1
# Step 6: Verify
if is_updated_system_app; then
log "Messages Mod+ applied successfully!"
log "Messages is now running as a user-space app"
echo "RESULT=success"
return 0
else
err "Mod may not have applied correctly - check package state"
echo "RESULT=uncertain"
return 1
fi
}
cmd_uninstall() {
log "Reverting Messages to stock system app..."
if ! pm list packages | grep -q "$MSG_PKG"; then
err "Google Messages not installed"
return 1
fi
if ! is_updated_system_app; then
log "Messages is already running as stock system app"
echo "ALREADY_STOCK=true"
return 0
fi
# Uninstall updates reverts to the system version
log "Removing user-space updates..."
pm uninstall-updates "$MSG_PKG" 2>&1 || {
# Alternative method
pm install-existing "$MSG_PKG" 2>/dev/null
cmd install -r --user 0 "$MSG_PKG" 2>/dev/null
}
# Make sure it's enabled
pm enable "$MSG_PKG" 2>/dev/null
if ! is_updated_system_app; then
log "Successfully reverted to stock system Messages"
echo "RESULT=success"
return 0
else
err "Revert may not have completed - check package state"
echo "RESULT=uncertain"
return 1
fi
}
# Command router
case "$1" in
status) cmd_status ;;
backup-sms) cmd_backup_sms ;;
backup-rcs) cmd_backup_rcs ;;
backup-attach) cmd_backup_attachments ;;
restore-sms) cmd_restore_sms "$2" ;;
restore-rcs) cmd_restore_rcs "$2" ;;
list-backups) cmd_list_backups "$2" ;;
delete-backup) cmd_delete_backup "$2" ;;
apply) cmd_apply_mod ;;
uninstall) cmd_uninstall ;;
*)
echo "Messages Mod+ v1.0.0"
echo ""
echo "Usage: $0 <command> [args]"
echo ""
echo "Commands:"
echo " status Show current Messages state"
echo " apply Apply mod (reinstall as user-space app)"
echo " uninstall Revert to stock system app"
echo " backup-sms Backup SMS/MMS database"
echo " backup-rcs Backup RCS database"
echo " backup-attach Backup message attachments"
echo " restore-sms [f] Restore SMS/MMS database (latest or specific file)"
echo " restore-rcs [f] Restore RCS database (latest or specific file)"
echo " list-backups <t> List backups (sms|rcs|attachments)"
echo " delete-backup <p> Delete a backup file or directory"
echo ""
exit 1
;;
esac

15
uninstall.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/system/bin/sh
# Messages Mod+ - Module Uninstall Hook
# Reverts Messages to stock when the module is removed from KernelSU
MSG_PKG="com.google.android.apps.messaging"
# Check if Messages is in the updated state and revert
if dumpsys package "$MSG_PKG" | grep "pkgFlags" | head -1 | grep -q "UPDATED_SYSTEM_APP"; then
echo "[MessagesModPlus] Reverting Messages to stock system app..."
pm uninstall-updates "$MSG_PKG" 2>/dev/null
pm enable "$MSG_PKG" 2>/dev/null
echo "[MessagesModPlus] Messages reverted to stock"
fi
echo "[MessagesModPlus] Module uninstalled. Backups remain at /sdcard/MessagesModPlus/"

713
webroot/index.html Normal file
View File

@@ -0,0 +1,713 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Messages Mod+</title>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #232733;
--border: #2d3140;
--text: #e4e6ef;
--text2: #8b8fa3;
--accent: #6c8cff;
--accent-dim: rgba(108, 140, 255, 0.12);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.12);
--red: #f87171;
--red-dim: rgba(248, 113, 113, 0.12);
--orange: #fb923c;
--orange-dim: rgba(251, 146, 60, 0.12);
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 16px;
padding-bottom: 100px;
-webkit-user-select: none;
user-select: none;
}
.header {
text-align: center;
padding: 24px 0 20px;
}
.header h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.3px;
}
.header p {
color: var(--text2);
font-size: 13px;
margin-top: 4px;
}
.status-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px;
margin-bottom: 16px;
}
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
}
.status-row:not(:last-child) {
border-bottom: 1px solid var(--border);
margin-bottom: 4px;
padding-bottom: 10px;
}
.status-label {
font-size: 13px;
color: var(--text2);
}
.status-value {
font-size: 13px;
font-weight: 600;
}
.badge {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-modded { background: var(--green-dim); color: var(--green); }
.badge-system { background: var(--orange-dim); color: var(--orange); }
.badge-unknown { background: var(--surface2); color: var(--text2); }
.badge-error { background: var(--red-dim); color: var(--red); }
.section {
margin-bottom: 16px;
}
.section-title {
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text2);
padding: 0 4px;
margin-bottom: 8px;
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
.btn {
display: flex;
align-items: center;
width: 100%;
padding: 14px 16px;
border: none;
background: transparent;
color: var(--text);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
text-align: left;
gap: 12px;
}
.btn:active { background: var(--surface2); }
.btn:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.btn-icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.btn-content {
flex: 1;
min-width: 0;
}
.btn-subtitle {
font-size: 11px;
color: var(--text2);
margin-top: 2px;
}
.btn-chevron {
color: var(--text2);
font-size: 18px;
flex-shrink: 0;
}
.bg-blue { background: var(--accent-dim); }
.bg-green { background: var(--green-dim); }
.bg-red { background: var(--red-dim); }
.bg-orange { background: var(--orange-dim); }
.primary-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 15px;
border: none;
border-radius: var(--radius);
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 600;
cursor: pointer;
gap: 8px;
transition: opacity 0.15s;
margin-bottom: 16px;
}
.primary-btn:active { opacity: 0.8; }
.primary-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.primary-btn.danger {
background: var(--red);
}
/* Log output area */
.log-area {
display: none;
background: #0a0c10;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
margin-bottom: 16px;
max-height: 300px;
overflow-y: auto;
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--text2);
white-space: pre-wrap;
word-break: break-all;
}
.log-area.visible { display: block; }
.log-line-info { color: var(--accent); }
.log-line-ok { color: var(--green); }
.log-line-err { color: var(--red); }
/* Backup list modal */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 100;
align-items: flex-end;
justify-content: center;
}
.modal-overlay.visible {
display: flex;
}
.modal {
background: var(--surface);
border-radius: 16px 16px 0 0;
width: 100%;
max-height: 70vh;
overflow: hidden;
animation: slideUp 0.25s ease-out;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
font-size: 16px;
font-weight: 600;
}
.modal-close {
background: var(--surface2);
border: none;
color: var(--text);
width: 30px;
height: 30px;
border-radius: 50%;
font-size: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.modal-body {
padding: 8px 0;
overflow-y: auto;
max-height: calc(70vh - 60px);
}
.backup-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border);
gap: 12px;
}
.backup-info {
flex: 1;
min-width: 0;
}
.backup-name {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.backup-size {
font-size: 11px;
color: var(--text2);
margin-top: 2px;
}
.backup-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.backup-actions button {
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface2);
color: var(--text);
font-size: 12px;
font-weight: 500;
cursor: pointer;
}
.backup-actions button.restore-btn {
border-color: var(--accent);
color: var(--accent);
}
.backup-actions button.delete-btn {
border-color: var(--red);
color: var(--red);
}
.empty-state {
text-align: center;
padding: 32px 16px;
color: var(--text2);
font-size: 13px;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="header">
<h1>Messages Mod+</h1>
<p>Fix RCS on rooted devices</p>
</div>
<!-- Status Card -->
<div class="status-card" id="statusCard">
<div class="status-row">
<span class="status-label">Messages State</span>
<span class="badge badge-unknown" id="stateBadge">Checking...</span>
</div>
<div class="status-row">
<span class="status-label">Package</span>
<span class="status-value" id="stateEnabled" style="color:var(--text2)">--</span>
</div>
<div class="status-row">
<span class="status-label">Backups</span>
<span class="status-value" id="stateBackups" style="color:var(--text2)">--</span>
</div>
</div>
<!-- Apply / Revert -->
<button class="primary-btn" id="applyBtn" onclick="applyMod()" disabled>
Apply Mod
</button>
<!-- Log Output -->
<div class="log-area" id="logArea"></div>
<!-- Backup Section -->
<div class="section">
<div class="section-title">Backup</div>
<div class="card">
<button class="btn" onclick="runBackup('sms')">
<div class="btn-icon bg-blue">&#128172;</div>
<div class="btn-content">
<div>SMS / MMS Database</div>
<div class="btn-subtitle">Backup text and multimedia messages</div>
</div>
<div class="btn-chevron">&#8250;</div>
</button>
<button class="btn" onclick="runBackup('rcs')">
<div class="btn-icon bg-green">&#9889;</div>
<div class="btn-content">
<div>RCS Database</div>
<div class="btn-subtitle">Backup chat features / RCS messages</div>
</div>
<div class="btn-chevron">&#8250;</div>
</button>
<button class="btn" onclick="runBackup('attach')">
<div class="btn-icon bg-orange">&#128206;</div>
<div class="btn-content">
<div>Attachments</div>
<div class="btn-subtitle">Save photos, videos, and media</div>
</div>
<div class="btn-chevron">&#8250;</div>
</button>
</div>
</div>
<!-- Restore Section -->
<div class="section">
<div class="section-title">Restore</div>
<div class="card">
<button class="btn" onclick="showBackups('sms')">
<div class="btn-icon bg-blue">&#128259;</div>
<div class="btn-content">
<div>Restore SMS / MMS</div>
<div class="btn-subtitle">Restore from a previous backup</div>
</div>
<div class="btn-chevron">&#8250;</div>
</button>
<button class="btn" onclick="showBackups('rcs')">
<div class="btn-icon bg-green">&#128259;</div>
<div class="btn-content">
<div>Restore RCS</div>
<div class="btn-subtitle">Restore RCS messages database</div>
</div>
<div class="btn-chevron">&#8250;</div>
</button>
</div>
</div>
<!-- Danger Zone -->
<div class="section">
<div class="section-title">Danger Zone</div>
<button class="primary-btn danger" id="uninstallBtn" onclick="uninstallMod()">
Revert to Stock
</button>
</div>
<!-- Backup List Modal -->
<div class="modal-overlay" id="modalOverlay" onclick="closeModal(event)">
<div class="modal">
<div class="modal-header">
<h3 id="modalTitle">Backups</h3>
<button class="modal-close" onclick="closeModal()">&times;</button>
</div>
<div class="modal-body" id="modalBody">
<div class="empty-state">Loading...</div>
</div>
</div>
</div>
<script>
const SCRIPT_PATH = '/data/adb/modules/messages_mod_plus/scripts/messages_mod.sh';
let busy = false;
async function exec(cmd) {
try {
const result = await ksu.exec(cmd);
return result;
} catch (e) {
return { errno: -1, stdout: '', stderr: e.message || 'exec failed' };
}
}
function logClear() {
const area = document.getElementById('logArea');
area.innerHTML = '';
area.classList.add('visible');
}
function logLine(text, type) {
const area = document.getElementById('logArea');
area.classList.add('visible');
const cls = type === 'err' ? 'log-line-err' : type === 'ok' ? 'log-line-ok' : 'log-line-info';
area.innerHTML += `<span class="${cls}">${escHtml(text)}</span>\n`;
area.scrollTop = area.scrollHeight;
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function setBusy(b) {
busy = b;
document.getElementById('applyBtn').disabled = b;
document.getElementById('uninstallBtn').disabled = b;
}
async function refreshStatus() {
const r = await exec(`sh ${SCRIPT_PATH} status`);
const lines = (r.stdout || '').split('\n');
const data = {};
lines.forEach(l => {
const [k, v] = l.split('=');
if (k && v !== undefined) data[k.trim()] = v.trim();
});
const badge = document.getElementById('stateBadge');
const state = data.STATE || 'UNKNOWN';
badge.className = 'badge';
if (state === 'MODDED') {
badge.classList.add('badge-modded');
badge.textContent = 'Modded';
document.getElementById('applyBtn').textContent = 'Already Applied';
document.getElementById('applyBtn').disabled = true;
} else if (state === 'SYSTEM') {
badge.classList.add('badge-system');
badge.textContent = 'System';
document.getElementById('applyBtn').textContent = 'Apply Mod';
document.getElementById('applyBtn').disabled = false;
} else if (state === 'NOT_INSTALLED') {
badge.classList.add('badge-error');
badge.textContent = 'Not Installed';
document.getElementById('applyBtn').textContent = 'Messages Not Found';
document.getElementById('applyBtn').disabled = true;
} else {
badge.classList.add('badge-unknown');
badge.textContent = state;
document.getElementById('applyBtn').disabled = false;
}
const enabled = data.ENABLED === 'true';
document.getElementById('stateEnabled').textContent = enabled ? 'Enabled' : 'Disabled';
document.getElementById('stateEnabled').style.color = enabled ? 'var(--green)' : 'var(--red)';
const smsB = parseInt(data.SMS_BACKUPS) || 0;
const rcsB = parseInt(data.RCS_BACKUPS) || 0;
const attB = parseInt(data.ATTACH_BACKUPS) || 0;
document.getElementById('stateBackups').textContent = `${smsB} SMS, ${rcsB} RCS, ${attB} Attach`;
return data;
}
async function applyMod() {
if (busy) return;
setBusy(true);
logClear();
logLine('Applying Messages Mod+...', 'info');
const r = await exec(`sh ${SCRIPT_PATH} apply 2>&1`);
const output = r.stdout || '';
output.split('\n').forEach(line => {
if (!line.trim()) return;
if (line.includes('ERROR')) logLine(line, 'err');
else if (line.includes('success') || line.includes('Success')) logLine(line, 'ok');
else logLine(line, 'info');
});
if (r.stderr) logLine(r.stderr, 'err');
if (output.includes('RESULT=success') || output.includes('ALREADY_MODDED')) {
logLine('Done! Set Messages as default SMS app on your device.', 'ok');
} else {
logLine('Check output above for errors.', 'err');
}
await refreshStatus();
setBusy(false);
}
async function uninstallMod() {
if (busy) return;
setBusy(true);
logClear();
logLine('Reverting to stock Messages...', 'info');
const r = await exec(`sh ${SCRIPT_PATH} uninstall 2>&1`);
const output = r.stdout || '';
output.split('\n').forEach(line => {
if (!line.trim()) return;
if (line.includes('ERROR')) logLine(line, 'err');
else if (line.includes('success') || line.includes('ALREADY_STOCK')) logLine(line, 'ok');
else logLine(line, 'info');
});
if (r.stderr) logLine(r.stderr, 'err');
await refreshStatus();
setBusy(false);
}
async function runBackup(type) {
if (busy) return;
setBusy(true);
logClear();
const labels = { sms: 'SMS/MMS', rcs: 'RCS', attach: 'Attachments' };
logLine(`Backing up ${labels[type]}...`, 'info');
const cmd = type === 'attach' ? 'backup-attach' : `backup-${type}`;
const r = await exec(`sh ${SCRIPT_PATH} ${cmd} 2>&1`);
const output = r.stdout || '';
output.split('\n').forEach(line => {
if (!line.trim()) return;
if (line.includes('ERROR')) logLine(line, 'err');
else if (line.includes('BACKUP=')) logLine('Saved: ' + line.split('=')[1], 'ok');
else logLine(line, 'info');
});
if (r.stderr) logLine(r.stderr, 'err');
await refreshStatus();
setBusy(false);
}
async function showBackups(type) {
const overlay = document.getElementById('modalOverlay');
const title = document.getElementById('modalTitle');
const body = document.getElementById('modalBody');
title.textContent = type === 'sms' ? 'SMS/MMS Backups' : 'RCS Backups';
body.innerHTML = '<div class="empty-state">Loading...</div>';
overlay.classList.add('visible');
const r = await exec(`sh ${SCRIPT_PATH} list-backups ${type}`);
const lines = (r.stdout || '').trim().split('\n').filter(l => l.includes('|'));
if (lines.length === 0) {
body.innerHTML = '<div class="empty-state">No backups found.<br>Create one from the Backup section above.</div>';
return;
}
let html = '';
lines.forEach(line => {
const parts = line.split('|');
const path = parts[0];
const name = parts[1];
const size = parts[2];
html += `
<div class="backup-item">
<div class="backup-info">
<div class="backup-name">${escHtml(name)}</div>
<div class="backup-size">${escHtml(size)}</div>
</div>
<div class="backup-actions">
<button class="restore-btn" onclick="restoreBackup('${type}', '${escAttr(path)}')">Restore</button>
<button class="delete-btn" onclick="deleteBackup('${escAttr(path)}', this)">Delete</button>
</div>
</div>`;
});
body.innerHTML = html;
}
function escAttr(s) {
return s.replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
async function restoreBackup(type, path) {
if (busy) return;
closeModal();
setBusy(true);
logClear();
logLine(`Restoring ${type.toUpperCase()} from backup...`, 'info');
const r = await exec(`sh ${SCRIPT_PATH} restore-${type} '${path}' 2>&1`);
const output = r.stdout || '';
output.split('\n').forEach(line => {
if (!line.trim()) return;
if (line.includes('ERROR')) logLine(line, 'err');
else if (line.includes('RESTORED=')) logLine('Restored from: ' + line.split('=')[1], 'ok');
else logLine(line, 'info');
});
if (r.stderr) logLine(r.stderr, 'err');
logLine('Restart Messages or reboot for changes to take effect.', 'info');
setBusy(false);
}
async function deleteBackup(path, btnEl) {
const r = await exec(`sh ${SCRIPT_PATH} delete-backup '${path}' 2>&1`);
if (r.stdout && r.stdout.includes('Deleted')) {
const item = btnEl.closest('.backup-item');
if (item) item.remove();
const body = document.getElementById('modalBody');
if (!body.querySelector('.backup-item')) {
body.innerHTML = '<div class="empty-state">No backups found.</div>';
}
refreshStatus();
}
}
function closeModal(event) {
if (event && event.target !== event.currentTarget) return;
document.getElementById('modalOverlay').classList.remove('visible');
}
// Init
document.addEventListener('DOMContentLoaded', () => {
refreshStatus();
});
</script>
</body>
</html>