Add previous code

This commit is contained in:
Antonio J. Delgado 2025-07-14 21:09:23 +03:00
parent d75a32a2d1
commit 29e47d2a5c
2 changed files with 244 additions and 2 deletions

View file

@ -9,10 +9,15 @@ import sys
import os import os
import json import json
import time import time
import re
import logging import logging
from logging.handlers import SysLogHandler from logging.handlers import SysLogHandler
import click import click
import click_config_file import click_config_file
from ppadb.client import Client as AdbClient
import ppadb
import requests
import yaml
HOME_FOLDER = os.environ.get('HOME', os.environ.get('USERPROFILE', '/')) HOME_FOLDER = os.environ.get('HOME', os.environ.get('USERPROFILE', '/'))
@ -46,6 +51,210 @@ class AndroidManager:
"last_update": 0, "last_update": 0,
} }
self.data = self._read_cached_data() self.data = self._read_cached_data()
self.lines = None
self.client = AdbClient(host=self.config['host'], port=self.config['port'])
self.devices = []
for device_object in self.client.devices():
dev = {}
dev['object'] = device_object
try:
dev['properties'] = device_object.get_properties()
dev['name'] = dev['properties']['ro.semc.product.name']
device_object.shell('dumpsys bluetooth_manager', handler=self._handle_output)
for line in self.lines:
match = re.search(r'^ Name: (.*)$', line)
if match:
dev['name'] = match.group(1)
match = re.search(r'^ address: (.*)$', line)
if match:
dev['bt_address'] = match.group(1)
self.devices.append(dev)
self._log.debug(
"Found Android device: %s",
dev['name']
)
except Exception as error:
self._log.error(f"Error reading device properties. Maybe the device is off-line? {error}")
self.get_installed_apps()
def get_installed_apps(self, device_name=None):
'''Get a list of installed app in a device or all'''
for device in self.devices:
if device_name == device['name'] or device_name is None:
device['object'].shell("pm list packages -3 -f", handler=self._handle_output)
device['apps'] = []
for line in self.lines:
app = {}
match = re.search(r'^package:(.*\.apk)=(.*)$', line)
if match:
app['path'] = os.path.dirname(match.group(1))
app['id'] = match.group(2)
else:
self._log.error(
"RegExp failed to identify app in line: '%s'",
line
)
device['apps'].append(app)
def restore_apps(self, backup_text_file, device_product_name):
'''Restore apps from a text file using F-Droid repository'''
if not os.path.exists(backup_text_file):
self._log.error(
"The backup file '%s' doesn't exist.",
backup_text_file
)
return False
session = requests.Session()
with open(backup_text_file, 'r', encoding='utf-8') as backup_file:
content = backup_file.read()
for app_code in content.split():
self._log.info(
"Processing app '%s' for restoration...",
app_code
)
app_url = f"https://gitlab.com/fdroid/fdroiddata/-/raw/master/metadata/{app_code}.yml"
result = session.get(app_url)
if result.status_code > 399:
self._log.error(
"Error %s fetching app data file from '%s'.",
result.status_code,
app_url
)
continue
app_data = yaml.load(result.content, Loader=yaml.Loader)
latest_build = 0
if not app_data:
self._log.error(
"Error loading YAML data from remote application file '%s'. Result: %s",
app_url,
result.content
)
continue
if 'Builds' not in app_data:
self._log.error(
"'Builds' section not found in application data. %s",
app_data
)
continue
for build in app_data['Builds']:
if build['versionCode'] > latest_build:
latest_build = build['versionCode']
if latest_build == 0:
self._log.error(
"Unable to find the latest version code in the app data file '%s'",
app_url
)
continue
self._log.debug(
"Latest build version code name seems to be '%s'",
latest_build
)
build_url = f"https://verification.f-droid.org/{app_code}_{latest_build}.apk.json"
result = session.get(
build_url
)
if result.status_code > 399:
self._log.error(
"Error %s fetching build information from file '%s'.",
result.status_code,
build_url
)
continue
build_data = json.loads(result.content)
apk_file_url = None
for build in build_data.keys():
apk_file_url = build_data[build]['url']
self._log.debug(
"Found file: %s",
apk_file_url
)
if not apk_file_url:
self._log.error(
"Error finding APK file in '%s'. %s",
build_url,
result.content
)
continue
result = session.get(
apk_file_url
)
apk_file = os.path.join(os.path.dirname(backup_text_file), f"{app_code}.apk")
target_device = None
with open(apk_file, 'w+b') as apk_object_file:
apk_object_file.write(result.content)
if device_product_name:
for device in self.devices:
if device['name'] == device_product_name:
target_device = device
else:
target_device=self.devices[0]
if target_device:
self._log.debug(
"Installing app '%s' with version code '%s' to device '%s'...",
app_code,
latest_build,
target_device['name']
)
try:
target_device['object'].install(apk_file)
self._log.debug(
"Installed without errors."
)
except ppadb.InstallError as error:
if 'INSTALL_FAILED_ALREADY_EXISTS' in error:
self._log.debug(
'App already installed, not installing again. %s',
error
)
else:
self._log.error(
"Error installing application '%s' with version code '%s' to device '%s'. %s",
app_code,
latest_build,
target_device['name'],
error
)
return True
def backup_apps(self, backup_path, just_names):
'''Backup apps either to a text file or to a APK files'''
for device in self.devices:
local_directory = os.path.join(backup_path, device.get('name', 'unknown_device'))
if not os.path.exists(local_directory):
os.mkdir(local_directory)
if just_names and os.path.exists(os.path.join(local_directory, 'apks_list.txt')):
os.remove(os.path.join(local_directory, 'apks_list.txt'))
for app in device['apps']:
remote_filename = f"{app['path']}/base.apk"
if not just_names:
local_filename = os.path.join(local_directory, f"{app['id']}.apk")
self._log.info(
"Saving '%s' into '%s'...",
remote_filename,
local_filename
)
device['object'].pull(remote_filename, local_filename)
else:
file_name = os.path.join(local_directory, 'apks_list.txt')
self._log.info(
"Saving list of apps into file '%s'...",
file_name
)
with open(file_name, 'a', encoding='UTF-8') as list_file:
list_file.write(f"{app['id']}\n")
def _handle_output(self, connection):
buffer = ""
while True:
data = connection.read(1024)
if not data:
break
# print(data)
buffer += data.decode('utf-8', errors='ignore')
connection.close()
self.lines = buffer.strip().split('\n')
def close(self): def close(self):
'''Close class and save data''' '''Close class and save data'''
@ -116,7 +325,7 @@ class AndroidManager:
return True return True
@click.command() @click.group()
@click.option( @click.option(
"--debug-level", "--debug-level",
"-d", "-d",
@ -145,9 +354,39 @@ class AndroidManager:
default=60*60*24*7, default=60*60*24*7,
help='Max age in seconds for the cache' help='Max age in seconds for the cache'
) )
@click.option('--host', '-H', default='127.0.0.1', help='ADB server host name or IP.')
@click.option('--port', '-p', default=5037, help='ADB server port to use.')
# @click.option("--dummy","-n", is_flag=True, # @click.option("--dummy","-n", is_flag=True,
# help="Don't do anything, just show what would be done.") # help="Don't do anything, just show what would be done.")
@click_config_file.configuration_option() @click_config_file.configuration_option()
@click.pass_context
def cli(ctx, **kwargs):
ctx.ensure_object(dict)
ctx.obj['debug_level'] = kwargs['debug_level']
ctx.obj['android_manager'] = AndroidManager(**kwargs)
@cli.command()
@click.option('--backup-path', '-p', required=True, help='Path to save the backed APKs')
@click.option('--just-names', '-n', is_flag=True, help='Save only the filename')
@click_config_file.configuration_option()
@click.pass_context
def backup_apps(ctx, backup_path, just_names):
'''Backup Android apps'''
ctx.obj['android_manager'].backup_apps(backup_path, just_names)
@cli.command()
@click.option('--backup-text-file', '-p', required=True, help='Path to save the backed APKs')
@click.option(
'--device-product-name',
'-n',
help='Device product name to install the apps. If not indicated it will install in the first device found.'
)
@click_config_file.configuration_option()
@click.pass_context
def restore_apps(ctx, backup_text_file, device_product_name):
'''Restore Android apps'''
ctx.obj['android_manager'].restore_apps(backup_text_file, device_product_name)
def __main__(**kwargs): def __main__(**kwargs):
obj = AndroidManager(**kwargs) obj = AndroidManager(**kwargs)
obj.close() obj.close()

View file

@ -1,2 +1,5 @@
click click
click_config_file click_config_file
pure-python-adb
requests
pyyaml