├── .vscode └── settings.json ├── language ├── translator.py ├── generate.py └── merge.py ├── logcat ├── .DS_Store ├── analyse.py └── collect.py ├── bin └── apktool │ ├── apktool.jar │ └── apktool.sh ├── .gitignore ├── config ├── languages.json ├── app.json └── baidu.json ├── apktool ├── config.json ├── godeye.py ├── apktool.py └── smali.py ├── files ├── textfiles.py ├── jsonfiles.py └── xmlfiles.py ├── global_config.py ├── config.py ├── launcher.py ├── README.md ├── android ├── pm.py ├── adb.py ├── am.py ├── am.md └── pm.md ├── generator.py ├── initializer.py ├── repository.py ├── translator.py ├── file_operator.py ├── importer.py ├── app_gui.py └── LICENSE /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "/usr/local/bin/python3" 3 | } -------------------------------------------------------------------------------- /language/translator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | -------------------------------------------------------------------------------- /language/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /logcat/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/AndroidTools/HEAD/logcat/.DS_Store -------------------------------------------------------------------------------- /bin/apktool/apktool.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shouheng88/AndroidTools/HEAD/bin/apktool/apktool.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python: 2 | *.py[cod] 3 | *.so 4 | *.egg 5 | *.egg-info 6 | *.xls 7 | *.xml 8 | dist 9 | build 10 | __pycahce__ -------------------------------------------------------------------------------- /config/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_title": { 3 | "zh": "Translate My App", 4 | "en": "Translate My App" 5 | } 6 | } -------------------------------------------------------------------------------- /apktool/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": "", 3 | "keywords": ["sysdebug"], 4 | "traceback": true, 5 | "traceback_generation": 3, 6 | "result_store_path": "." 7 | } 8 | -------------------------------------------------------------------------------- /files/textfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | def read_text(fname) -> str: 5 | '''Read text file full content.''' 6 | with open(fname, 'r') as f: 7 | content = f.read() 8 | f.close() 9 | return content 10 | -------------------------------------------------------------------------------- /files/jsonfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | 6 | def write_json(path, json_obj): 7 | '''Write json object to json file.''' 8 | json_str = json.dumps(json_obj) 9 | with open(path, "w") as f: 10 | f.write(json_str) 11 | 12 | def read_json(path): 13 | '''Read json object from json file.''' 14 | with open(path, "r") as f: 15 | return json.load(f) 16 | -------------------------------------------------------------------------------- /global_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | 6 | def config_logging(filename: str = 'app.log'): 7 | '''Config loggin library globaly.''' 8 | LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" 9 | DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" 10 | logging.basicConfig(filename=filename, filemode='a', level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT) 11 | logging.FileHandler(filename=filename, encoding='utf-8') 12 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | API_URL = 'http://api.fanyi.baidu.com/api/trans/vip/translate' # 百度语音识别 API 5 | BAIDU_CONFIG_URL = "config/baidu.json" 6 | APP_CONFIG_PATH = "config/app.json" # 应用配置文件 7 | REPO_CONFIG_PATH = "config/repo.json" # 仓库配置文件地址 8 | REPO_CONFIG_PATH_PREFIX = "config/repo_%s.json" 9 | APP_LANGUAGE_CONFIG_PATH = "config/languages.json" # 应用多语言配置文件 10 | TRANSLATE_EXCEL_SHEET_NAME = "Translation" 11 | TRANSLATE_EXCEL_FILE_NAME = "Translations.xlsx" 12 | -------------------------------------------------------------------------------- /config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "zh", 3 | "support_languages": [], 4 | "languages": { 5 | "中文简体": ["zh", "zh-rCN"], 6 | "中文(台湾)": ["zh-rTW"], 7 | "中文(香港)": ["zh-rHK"], 8 | "德语": ["de-rAT", "de-rCH", "de-rDE", "de-rLI"], 9 | "法语": ["fr-rBE", "fr-rCA", "fr-rCH", "fr-rFR"], 10 | "日语": ["ja"], 11 | "英语": [ 12 | "en", 13 | "en-rUS", 14 | "en-rGB", 15 | "en-rAU", 16 | "en-rCA", 17 | "en-rIE", 18 | "en-rIN", 19 | "en-rNZ", 20 | "en-rSG", 21 | "en-rZA" 22 | ], 23 | "韩语": ["ko", "ko-rKR"] 24 | }, 25 | "ios_language_black_list": [], 26 | "android_language_black_list": [], 27 | "ios_resources_root_directory": "", 28 | "android_resources_root_directory": "", 29 | "translate_excel_output_directory": "" 30 | } 31 | -------------------------------------------------------------------------------- /launcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import tkinter 6 | from tkinter import Tk 7 | from initializer import LanguageFactory 8 | from initializer import Initializer 9 | from app_gui import MainDialog 10 | from app_gui import RepoInitDialog 11 | 12 | def __config_logging(): 13 | '''配置日志''' 14 | LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" 15 | DATE_FORMAT = "%m/%d/%Y %H:%M:%S %p" 16 | logging.basicConfig(filename='app.log', filemode='a', level=logging.DEBUG, format=LOG_FORMAT, datefmt=DATE_FORMAT) 17 | logging.FileHandler(filename='app.log', encoding='utf-8') 18 | 19 | if __name__ == "__main__": 20 | __config_logging() 21 | # 加载多语言资源 22 | factory = LanguageFactory() 23 | factory.load_languages() 24 | # Tk 25 | root = Tk() 26 | root.title(factory.get_entry("app_title")) 27 | # 进行项目初始化 28 | initializer = Initializer() 29 | if not initializer.is_repo_initialized() : 30 | # 进入项目初始化页面 31 | RepoInitDialog(root).pack() 32 | else: 33 | # 进入正常编辑页面 34 | MainDialog(root).pack() 35 | root.mainloop() 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android 脚本工具合集 2 | 3 | 汇集了开发过程中会用到的多种脚本工具。 4 | 5 | ## 1、多语言管理工具 6 | 7 | ### 1.1 多语言合并脚本工具 8 | 9 | 用来将一个多语言资源文件合并到另一个多语言资源文件。比如,将别人翻译或者修改的多语言合并到主干。该脚本通过对 key 对比实现合并,不改变之前多语言资源的顺序。 10 | 11 | 进入 language 目录,执行命令, 12 | 13 | ``` 14 | python merge.py -f 被合并的多语言资源文件位置 -t 合并到的多语言资源文件位置 15 | ``` 16 | 17 | ### 1.2 根据 Android 多语言资源生成 iOS 多语言文件 18 | 19 | 根据 Android 的多语言资源文件和目录,生成 iOS 对应的多语言资源文件或者目录。 20 | 21 | ``` 22 | python generate.py -f 用来生成的资源文件 -o 输出到的位置 23 | ``` 24 | 25 | ### 1.3 将多语言文件翻译成其他语言 26 | 27 | 翻译多语言文件成其他语言,支持指定被翻译多语言文件和输出到的位置,如果已经存在指定的词条,则无需翻译,只对没有翻译结果的进行翻译。 28 | 29 | ``` 30 | python translate.py -f 被翻译的资源文件 -o 输出到的位置 31 | ``` 32 | 33 | 也可以直接指定要翻译的多语言的目录,此时根据目录名自动识别语言类型,然后根据默认多语言,补充和翻译不存在的词条, 34 | 35 | ``` 36 | python translate.py -f 被翻译的资源的目录 37 | ``` 38 | 39 | ### 1.4 根据 Android 多语言资源生成 Excel 40 | 41 | 根据 Android 多语言资源文件或者目录生成 Excel,如果传入的参数是文件只生成其自己对应的 Excel;如果传入的是目录,则每个语种对应的文件生成一个 sheet: 42 | 43 | ``` 44 | python genexcel.py -f 用来生成的文件或者目录 45 | ``` 46 | 47 | ## 2、日志采集和分析工具 48 | 49 | ### 2.1 日志采集工具 50 | 51 | 自动采集 Android 某个应用或者进程的日志并输出到文件中,便于对日志文件进行分析。使用:进入 logcat 目录,执行命令, 52 | 53 | ``` 54 | python collect.py -p 你的包名 -l 输出日志文件位置 -f yes 55 | ``` 56 | 57 | ### 2.2 日志分析工具 58 | 59 | 对上述采集到的日志文件进行分析,从大到小输出打印最多的日志等。使用:进入 logcat 目录,执行命令, 60 | 61 | ``` 62 | python analyse.py -f 日志文件地址 -p 包名 63 | ``` 64 | 65 | ## 3、上帝之眼 66 | 67 | -------------------------------------------------------------------------------- /apktool/godeye.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | ///////////////////////////////////////// ......... 6 | // .. ..... .. 7 | // The Android Smali File Searcher ... ........ ... 8 | // ... ......... ... 9 | // @Author Shouheng Wang .............. ... 10 | // ............... 11 | ///////////////////////////////////////// ......... 12 | ''' 13 | 14 | import os, sys, time, shutil, logging 15 | from typing import List 16 | from smali import SmaliSearcherConfiguration 17 | sys.path.insert(0, '../') 18 | import global_config 19 | from files.jsonfiles import read_json 20 | 21 | SMALI_SEARCHER_CONFIGURATION_FILE = "config.json" 22 | 23 | def read_configuration() -> SmaliSearcherConfiguration: 24 | '''Read smali searcher configuration.''' 25 | json_object = read_json(SMALI_SEARCHER_CONFIGURATION_FILE) 26 | configuration = SmaliSearcherConfiguration() 27 | configuration.package = json_object["package"] 28 | configuration.keywords = json_object["keywords"] 29 | configuration.traceback = json_object["traceback"] 30 | configuration.traceback_generation = json_object["traceback_generation"] 31 | return configuration 32 | 33 | 34 | 35 | if __name__ == "__main__": 36 | '''Program entrance.''' 37 | global_config.config_logging('../log/app.log') 38 | -------------------------------------------------------------------------------- /config/baidu.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_id": "your_app_id", 3 | "app_secret": "your_app_secret", 4 | "mappings": { 5 | "zh": "zh", 6 | "ja": "jp", 7 | "zh-rCN": "cht", 8 | "zh-rTW": "cht", 9 | "zh-rHK": "yue", 10 | "en": "en", 11 | "en-rUS": "en", 12 | "en-rGB": "en", 13 | "en-rAU": "en", 14 | "en-rCA": "en", 15 | "en-rIE": "en", 16 | "en-rIN": "en", 17 | "en-rNZ": "en", 18 | "en-rSG": "en", 19 | "en-rZA": "en", 20 | "ar-rEG": "ara", 21 | "ar-rIL": "ara", 22 | "de-rAT": "de", 23 | "de-rCH": "de", 24 | "de-rDE": "de", 25 | "de-rLI": "de", 26 | "ko-rKR": "kor", 27 | "ko": "kor", 28 | "th-rTH": "th", 29 | "pt-rBR": "pt", 30 | "pt-rPT": "pt", 31 | "el-rGR": "el", 32 | "bg-rBG": "bul", 33 | "fi-rFI": "fin", 34 | "sl-rSI": "slo", 35 | "fr-rBE": "fra", 36 | "fr-rCA": "fra", 37 | "fr-rCH": "fra", 38 | "fr-rFR": "fra", 39 | "nl-BE": "nl", 40 | "nl-rNL": "nl", 41 | "cs-rCZ": "cs", 42 | "sv-rSE": "swe", 43 | "vi-rVN": "vie", 44 | "es-rES": "spa", 45 | "es-rUS": "spa", 46 | "es": "spa", 47 | "ru": "ru", 48 | "ru-rRU": "ru", 49 | "it-rCH": "it", 50 | "it-rIT": "it", 51 | "pl-rPL": "pl", 52 | "da-rDK": "dan", 53 | "ro-rRO": "rom", 54 | "hu-rHU": "hu" 55 | } 56 | } -------------------------------------------------------------------------------- /bin/apktool/apktool.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2007 The Android Open Source Project 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | # This script is a wrapper for smali.jar, so you can simply call "smali", 18 | # instead of java -jar smali.jar. It is heavily based on the "dx" script 19 | # from the Android SDK 20 | 21 | # Set up prog to be the path of this script, including following symlinks, 22 | # and set up progdir to be the fully-qualified pathname of its directory. 23 | prog="$0" 24 | while [ -h "${prog}" ]; do 25 | newProg=`/bin/ls -ld "${prog}"` 26 | 27 | newProg=`expr "${newProg}" : ".* -> \(.*\)$"` 28 | if expr "x${newProg}" : 'x/' >/dev/null; then 29 | prog="${newProg}" 30 | else 31 | progdir=`dirname "${prog}"` 32 | prog="${progdir}/${newProg}" 33 | fi 34 | done 35 | oldwd=`pwd` 36 | progdir=`dirname "${prog}"` 37 | cd "${progdir}" 38 | progdir=`pwd` 39 | prog="${progdir}"/`basename "${prog}"` 40 | cd "${oldwd}" 41 | 42 | jarfile=apktool.jar 43 | libdir="$progdir" 44 | if [ ! -r "$libdir/$jarfile" ] 45 | then 46 | echo `basename "$prog"`": can't find $jarfile" 47 | exit 1 48 | fi 49 | 50 | javaOpts="" 51 | 52 | # If you want DX to have more memory when executing, uncomment the following 53 | # line and adjust the value accordingly. Use "java -X" for a list of options 54 | # you can pass here. 55 | # 56 | javaOpts="-Xmx512M -Dfile.encoding=utf-8" 57 | 58 | # Alternatively, this will extract any parameter "-Jxxx" from the command line 59 | # and pass them to Java (instead of to dx). This makes it possible for you to 60 | # add a command-line parameter such as "-JXmx256M" in your ant scripts, for 61 | # example. 62 | while expr "x$1" : 'x-J' >/dev/null; do 63 | opt=`expr "$1" : '-J\(.*\)'` 64 | javaOpts="${javaOpts} -${opt}" 65 | shift 66 | done 67 | 68 | if [ "$OSTYPE" = "cygwin" ] ; then 69 | jarpath=`cygpath -w "$libdir/$jarfile"` 70 | else 71 | jarpath="$libdir/$jarfile" 72 | fi 73 | 74 | # add current location to path for aapt 75 | PATH=$PATH:`pwd`; 76 | export PATH; 77 | exec java $javaOpts -Djava.awt.headless=true -jar "$jarpath" "$@" -------------------------------------------------------------------------------- /files/xmlfiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from xml.dom.minidom import parse 5 | import xml.dom.minidom 6 | import logging 7 | 8 | def read_android_strings(fname): 9 | '''Read android languages.''' 10 | dist = {} 11 | try: 12 | # Use minidom to open xml. 13 | DOMTree = xml.dom.minidom.parse(fname) 14 | except Exception: 15 | logging.error("Failed to read Android strings: " + fname) 16 | return dist 17 | collection = DOMTree.documentElement 18 | strings = collection.getElementsByTagName("string") 19 | index = 0 20 | for string in strings: 21 | index += 1 22 | try: 23 | keyword = string.getAttribute('name') 24 | translation = "" 25 | if len(string.childNodes) == 1: 26 | node = string.childNodes[0] 27 | # Handle CDATA label. 28 | if node.nodeType == 4: 29 | translation = "" 30 | else: 31 | translation = node.data 32 | else: 33 | # Handle child nodes. 34 | for node in string.childNodes: 35 | # Handle CDATA label. 36 | if node.nodeType == 4: 37 | translation = translation + "" 38 | else: 39 | translation = translation + node.toxml() 40 | # Do filter for some chars. 41 | if '\\\'' in translation: 42 | translation = translation.replace('\\\'', '\'') 43 | dist[keyword] = translation 44 | except BaseException as e: 45 | logging.error("Invalid entry at index " + str(index) + " " + str(keyword) + " : " + str(e)) 46 | return dist 47 | 48 | def write_android_resources(dist, fname): 49 | '''Write android string resources.''' 50 | content = '\n\n' 51 | for k, v in dist.items(): 52 | # Handle chars etc. 53 | if '\'' in v: 54 | v = v.replace("\'", "\\\'") 55 | # Handle > and < 56 | if ('>' in v or '<' in v) and '', '>') 58 | v = v.replace('<', '<') 59 | # Handle … 60 | if '…' in v and '' + v + '\n' 64 | content += '' 65 | with open(fname, 'w', encoding='utf-8') as f: 66 | f.write(content) 67 | -------------------------------------------------------------------------------- /language/merge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | //////////////////////////////////////////////////////////////////// 6 | // Android language files merge tool 7 | // Usage: 8 | // python merge.py 9 | // -f the_path_of_langauge_file_to_merge_from 10 | // -t the_path_of_language_file_to_merge_to 11 | // @Author: Mr.Shouheng 12 | //////////////////////////////////////////////////////////////////// 13 | ''' 14 | 15 | import sys, getopt 16 | import logging 17 | import sys 18 | sys.path.insert(0, '../') 19 | import global_config 20 | from files.xmlfiles import read_android_strings, write_android_resources 21 | 22 | command_info = "\ 23 | Options: \n\ 24 | -h[--help] Help info\n\ 25 | -f[--from] Languages file to merge from\n\ 26 | -t[--to] Languages file to merge to\ 27 | " 28 | 29 | def __show_invalid_command(info: str): 30 | '''Show invliad command info.''' 31 | print('Error: Unrecognized command: %s' % info) 32 | print(command_info) 33 | 34 | class MergeInfo: 35 | ''''Merge info object.''' 36 | def __init__(self, merge_from: str, merge_to: str): 37 | self.merge_from = merge_from 38 | self.merge_to = merge_to 39 | 40 | def __parse_command(argv) -> MergeInfo: 41 | '''Parse merge info from input command.''' 42 | try: 43 | # : and = means accept arguments 44 | opts, args = getopt.getopt(argv, "-h:-f:-t:", ["help", "from=", 'to=']) 45 | except BaseException as e: 46 | __show_invalid_command(str(e)) 47 | sys.exit(2) 48 | if len(opts) == 0: 49 | __show_invalid_command('empty parameters') 50 | return 51 | merge_from = merge_to = None 52 | for opt, arg in opts: 53 | if opt in ('-f', '--from'): 54 | merge_from = arg 55 | elif opt in ('-t', '--to'): 56 | merge_to = arg 57 | elif opt in ('-h', '--help'): 58 | print(command_info) 59 | return 60 | if merge_from == None or merge_to == None: 61 | __show_invalid_command('Launchage merge file required') 62 | return 63 | return MergeInfo(merge_from, merge_to) 64 | 65 | def __do_merge(info: MergeInfo): 66 | ''''Merge launguage files.''' 67 | strings_from = read_android_strings(info.merge_from) 68 | strings_to = read_android_strings(info.merge_to) 69 | for k, v in strings_from.items(): 70 | strings_to[k] = v 71 | write_android_resources(strings_to, info.merge_to) 72 | 73 | def __execute(argv): 74 | ''''Execute command.''' 75 | # Parse command. 76 | info = __parse_command(argv) 77 | if info == None: 78 | return 79 | logging.debug(">> Merging launguages from [" + str(info.merge_from) + "] to [" + str(info.merge_to)+ "].") 80 | __do_merge(info) 81 | 82 | if __name__ == "__main__": 83 | '''Program entrance.''' 84 | global_config.config_logging('../log/app.log') 85 | __execute(sys.argv[1:]) 86 | -------------------------------------------------------------------------------- /android/pm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # //////////////////////////////////////////////////////////////////// 5 | # 6 | # The Android Debug Bridge Wrapper Project by Python 7 | # 8 | # @Author Mr.Shouheng 9 | # 10 | # @References: 11 | # - https://developer.android.com/studio/command-line/adb?hl=zh-cn#am 12 | # 13 | # //////////////////////////////////////////////////////////////////// 14 | 15 | from adb import * 16 | from typing import List 17 | 18 | class PackageInfo: 19 | def __init__(self, pkg: str = '', path: str = '', uid: str = '', vcode: str = '') -> None: 20 | self.pkg = pkg 21 | self.path = path 22 | self.uid = uid 23 | self.vcode = vcode 24 | 25 | def __str__(self) -> str: 26 | return "(%s, %s, %s, %s)" % (self.pkg, self.path, self.uid, self.vcode) 27 | 28 | def get_packages(serial: str = None) -> List[PackageInfo]: 29 | '''Get all packages''' 30 | out = os_popen("adb %s shell pm list packages -f -U -u --show-versioncode ", serial) 31 | infos = [] 32 | for line in out.split("\n"): 33 | info = _parse_package_line(line) 34 | infos.append(info) 35 | return infos 36 | 37 | def get_package_info(pkg: str, serial: str = None) -> PackageInfo: 38 | '''Get info of one package.''' 39 | out = os_popen("adb %s shell pm list packages -f -U -u --show-versioncode | grep %s ", serial, pkg) 40 | return _parse_package_line(out) 41 | 42 | def download_apk_of_package(pkg: str, to: str, serial: str = None) -> int: 43 | '''Download APK of given package name to given path.''' 44 | info = get_package_info(pkg, serial) 45 | if len(info.path) > 0: 46 | return pull(info.path, to, serial) 47 | return -1 48 | 49 | def _parse_package_line(line: str) -> PackageInfo: 50 | '''Parse package info from line text.''' 51 | parts = line.split(" ") 52 | info = PackageInfo() 53 | for part in parts: 54 | if len(part) > 0: 55 | pairs = part.split(":") 56 | if len(pairs) >= 2: 57 | if pairs[0] == "package": 58 | index = pairs[1].rindex("=") 59 | info.path = pairs[1][0:index] 60 | info.pkg = pairs[1][index+1:] 61 | elif pairs[0] == "versionCode": 62 | info.vcode = pairs[1] 63 | elif pairs[0] == "uid": 64 | info.uid = pairs[1] 65 | return info 66 | 67 | if __name__ == "__main__": 68 | TEST_PACKAGE_NAME = "me.shouheng.leafnote" 69 | TEST_DEVICE_SERIAL = "emulator-5556" 70 | TEST_APK_PATH = "/Users/wangshouheng/downloads/Snapdrop-0.3.apk" 71 | TEST_LOCAL_FILE = "adb.py" 72 | TEST_REMOTE_DIR = "/sdcard" 73 | TEST_REMOTE_FILE = "/sdcard/adb.py" 74 | # print_list(get_packages(TEST_DEVICE_SERIAL)) 75 | # print(get_package_info(TEST_PACKAGE_NAME, TEST_DEVICE_SERIAL)) 76 | print(download_apk_of_package(TEST_PACKAGE_NAME, "~/leafnote.apk", TEST_DEVICE_SERIAL)) 77 | # os_system("adb %s shell pm > ./pm", TEST_DEVICE_SERIAL) 78 | -------------------------------------------------------------------------------- /generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import logging 6 | from repository import repository as repository 7 | from file_operator import ExcelOperator as ExcelOperator 8 | from file_operator import XmlOperator as XmlOperator 9 | from file_operator import FileOperator as FileOperator 10 | from config import TRANSLATE_EXCEL_SHEET_NAME 11 | from config import TRANSLATE_EXCEL_FILE_NAME 12 | 13 | # 文件生成工具类 14 | class Generator: 15 | # 初始化 16 | def __init__(self, appConfig): 17 | self.appConfig = appConfig 18 | 19 | # 根据需要翻译的词条生成用于翻译的 excel 20 | def gen_translate_excel(self, output_dir): 21 | repository.load() 22 | dist = {} 23 | for data in repository.datas: 24 | # 增加词条列 25 | keyword = data["keyword"] 26 | if "Keyword" not in dist: 27 | dist["Keyword"] = [] 28 | dist["Keyword"].append(keyword) 29 | # 增加翻译列 30 | translates = data["translates"] 31 | for k, v in translates.items(): 32 | if k not in dist: 33 | dist[k] = [] 34 | dist[k].append(v) 35 | # 生成 Excel 文件 36 | excelOperator = ExcelOperator() 37 | out_file = os.path.join(self.appConfig.translate_excel_output_directory, TRANSLATE_EXCEL_FILE_NAME) 38 | excelOperator.write_excel(dist, out_file) 39 | return True 40 | 41 | # 生成 Android 多语言资源 42 | def gen_android_resources(self): 43 | repository.load() 44 | android_blacklist = self.appConfig.android_language_black_list 45 | xmlOperator = XmlOperator() 46 | for language in repository.languages: 47 | # 过滤黑名单 48 | if language in android_blacklist: 49 | continue 50 | dist = {} 51 | for data in repository.datas: 52 | keyword = data["keyword"] 53 | translates = data["translates"] 54 | translation = translates[language] 55 | dist[keyword] = translation 56 | # 写入资源 57 | filename = "values" if language == "default" else "values-" + language 58 | language_dir = os.path.join(self.appConfig.android_resources_root_directory, filename) 59 | if not os.path.exists(language_dir): 60 | os.mkdir(language_dir) 61 | fname = os.path.join(language_dir, "strings.xml") 62 | xmlOperator.write_android_resources(dist, fname) 63 | 64 | # 生成 iOS 多语言资源 65 | def gen_ios_resources(self): 66 | repository.load() 67 | ios_blacklist = self.appConfig.ios_language_black_list 68 | fileOperator = FileOperator() 69 | for language in repository.languages: 70 | # 过滤黑名单 71 | if language in ios_blacklist: 72 | continue 73 | dist = {} 74 | for data in repository.datas: 75 | keyword = data["keyword"] 76 | translates = data["translates"] 77 | translation = translates[language] 78 | dist[keyword] = translation 79 | # 写入资源 80 | language_dir = os.path.join(self.appConfig.ios_resources_root_directory, language + ".lproj") 81 | if not os.path.exists(language_dir): 82 | os.mkdir(language_dir) 83 | fname = os.path.join(language_dir, "Localizable.strings") 84 | fileOperator.write_ios_resources(dist, fname) 85 | -------------------------------------------------------------------------------- /apktool/apktool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | /////////////////////////////////////////........................ 6 | // ........................ 7 | // The APK Tool Wrapper ........................ 8 | // ........................ 9 | /////////////////////////////////////////........................ 10 | ''' 11 | 12 | import os, sys, shutil, logging 13 | import hashlib 14 | import traceback 15 | from typing import List 16 | sys.path.insert(0, '../') 17 | import global_config 18 | 19 | SMALI_MIX_DIRECTORY = "smali_mix" 20 | ERROR_APK_FILE_NOT_EXISTS = "-1" 21 | 22 | class DecompileConfiguration: 23 | def __init__(self) -> None: 24 | # Should delete and re-decompile if given decompiled files or mixed smali files exists. 25 | self.force = False 26 | 27 | def decompile(apk: str, configuration: DecompileConfiguration) -> str: 28 | '''Decompile the APK.''' 29 | if not os.path.exists(apk): 30 | return ERROR_APK_FILE_NOT_EXISTS 31 | with open(apk, 'rb') as fp: 32 | data = fp.read() 33 | file_md5 = hashlib.md5(data).hexdigest() 34 | out = "./workspace_%s" % file_md5 35 | if os.path.exists(out) and not configuration.force: 36 | return os.path.join(out, SMALI_MIX_DIRECTORY) 37 | try: 38 | os.system("sh ../bin/apktool/apktool.sh D %s -o %s" % (apk, out)) 39 | except BaseException as e: 40 | logging.error('Error while decopiling:\n %s' % traceback.format_exc()) 41 | return out 42 | 43 | def mix_all_smalis(dir: str, configuration: DecompileConfiguration) -> str: 44 | ''' 45 | Mix all smali directories under given dir. 46 | ''' 47 | mix_to = os.path.join(dir, SMALI_MIX_DIRECTORY) 48 | if os.path.exists(mix_to) and not configuration.force: 49 | return mix_to 50 | files = os.listdir(dir) 51 | smalis = [] 52 | for f in files: 53 | if f.startswith("smali") and f != SMALI_MIX_DIRECTORY: 54 | # search_under_smali(os.path.join(dir, f), configuration) 55 | smalis.append(os.path.join(dir, f)) 56 | logging.info("Mixing " + str(smalis)) 57 | # Mix all smali files internal. 58 | return _mix_all_smalis(mix_to, smalis) 59 | 60 | def decompile_and_mix_all_smalis(apk: str, configuration: DecompileConfiguration) -> str: 61 | '''Decompile given apk, mix all smali files together and return the mixed path.''' 62 | decompile_dir = decompile(apk, configuration) 63 | if decompile_dir == ERROR_APK_FILE_NOT_EXISTS: 64 | return ERROR_APK_FILE_NOT_EXISTS 65 | else: 66 | return mix_all_smalis(decompile_dir, configuration) 67 | 68 | def _mix_all_smalis(mix_to: str, smalis: List[str]) -> str: 69 | ''' 70 | Mix all smali directories to one to reduce the complexity of the alogrithm. 71 | Also we can make a more interesting things based on mixed smalis. 72 | ''' 73 | progress = 1 74 | for smali in smalis: 75 | print(" >>> Mixing [%s] %d%%" % (smali, progress*100/len(smalis)), end = '\r') 76 | logging.info(" >>> Mixing [%s] %d%%" % (smali, progress*100/len(smalis))) 77 | try: 78 | shutil.copytree(smali, mix_to, dirs_exist_ok=True) 79 | except shutil.Error as e: 80 | for src, dst, msg in e.args[0]: 81 | print(dst, src, msg) 82 | progress = progress + 1 83 | return mix_to 84 | 85 | if __name__ == "__main__": 86 | '''Program entrance.''' 87 | global_config.config_logging('../log/app.log') 88 | configuration = DecompileConfiguration() 89 | dir = decompile_and_mix_all_smalis("/Users/wangshouheng/Desktop/apkanalyse/app_.apk", configuration) 90 | print(dir) 91 | -------------------------------------------------------------------------------- /logcat/analyse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | //////////////////////////////////////////////////////////////////// 6 | // Android logcat analyse tool 7 | // Usage: 8 | // python analyse.py 9 | // -f the_logcat_file_to_analyse 10 | // References: 11 | // - Description about logcat: 12 | // https://developer.android.com/studio/debug/am-logcat 13 | // @Author: Mr.Shouheng 14 | //////////////////////////////////////////////////////////////////// 15 | """ 16 | 17 | import sys, getopt 18 | import logging 19 | import sys 20 | import re 21 | import time 22 | from datetime import datetime 23 | from typing import List 24 | sys.path.insert(0, '../') 25 | import global_config 26 | 27 | LOGCAT_LINE_PATTERN = "^\d{1,2}-\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}\.\d{1,3}" 28 | 29 | command_info = "\ 30 | Options: \n\ 31 | -h[--help] Help info\n\ 32 | -f[--file] Log file to analyse\n\ 33 | -p[--package] Package name of analysing log\ 34 | " 35 | 36 | def __show_invalid_command(info: str): 37 | '''Show invliad command info.''' 38 | print('Error: Unrecognized command: %s' % info) 39 | print(command_info) 40 | 41 | class AnalyseInfo: 42 | ''''Analyse info object.''' 43 | def __init__(self, package: str, path: str): 44 | self.package = package 45 | self.path = path 46 | 47 | class AnalyseLineInfo: 48 | '''Line info.''' 49 | def __init__(self, time: int, pid: str, tid: str, level: str, tag: str) -> None: 50 | self.time = time 51 | self.pid = pid 52 | self.tid = tid 53 | self.level = level 54 | self.tag = tag 55 | 56 | def __parse_command(argv) -> AnalyseInfo: 57 | '''Parse analyse info from input command.''' 58 | try: 59 | # : and = means accept arguments 60 | opts, args = getopt.getopt(argv, "-h:-p:-f:", ["help", "package=", 'file=']) 61 | except BaseException as e: 62 | __show_invalid_command(str(e)) 63 | sys.exit(2) 64 | if len(opts) == 0: 65 | __show_invalid_command('empty parameters') 66 | return 67 | package = path = None 68 | for opt, arg in opts: 69 | if opt in ('-f', '--file'): 70 | path = arg 71 | elif opt in ('-p', '--package'): 72 | package = arg 73 | elif opt in ('-h', '--help'): 74 | print(command_info) 75 | return 76 | if package == None or path == None: 77 | __show_invalid_command('Package or log output path required!') 78 | return 79 | return AnalyseInfo(package, path) 80 | 81 | def __execute(argv): 82 | ''''Execute command.''' 83 | # Parse command. 84 | info = __parse_command(argv) 85 | if info == None: 86 | return 87 | logging.info(">> Collect logcat for package [" + info.package + "].") 88 | # Read and analyse logcat file 89 | __do_anlyse(info) 90 | 91 | def __do_anlyse(info: AnalyseInfo): 92 | '''Do analyse job.''' 93 | f = open(info.path) 94 | line = f.readline() 95 | tag_count = {} 96 | while line: 97 | matched = re.match(LOGCAT_LINE_PATTERN, line) 98 | if matched is not None: 99 | line_info = __do_analyse_line(line) 100 | if line_info.tag not in tag_count: 101 | tag_count[line_info.tag] = [] 102 | tag_count[line_info.tag].append(line_info) 103 | line = f.readline() 104 | f.close() 105 | for k, v in sorted(tag_count.items(), key = lambda kv:(len(kv[1]), len(kv[0]))): 106 | print(k + ": " + str(len(v))) 107 | 108 | def __do_analyse_line(line: str) -> AnalyseLineInfo: 109 | '''Analyse given line.''' 110 | time_str = re.match(LOGCAT_LINE_PATTERN, line).group(0) 111 | left = line.removeprefix(time_str).strip() 112 | index = left.find(' ') 113 | pid = left[0:index] 114 | left = left.removeprefix(pid).strip() 115 | index = left.find(' ') 116 | tid = left[0:index] 117 | left = left.removeprefix(tid).strip() 118 | index = left.find(' ') 119 | level = left[0:index] 120 | left = left.removeprefix(level).strip() 121 | index = left.find(':') 122 | tag = left[0:index] 123 | time_array = time.strptime('2021-' + time_str, "%Y-%m-%d %H:%M:%S.%f") 124 | time_stamp = int(time.mktime(time_array)) 125 | return AnalyseLineInfo(time_stamp, pid, tid, level, tag) 126 | 127 | if __name__ == "__main__": 128 | '''Program entrance.''' 129 | global_config.config_logging('../log/app.log') 130 | __execute(sys.argv[1:]) 131 | -------------------------------------------------------------------------------- /logcat/collect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | //////////////////////////////////////////////////////////////////// 6 | // Android logcat collect tool 7 | // Usage: 8 | // python collect.py 9 | // -f force restart or not 10 | // -p the_package_to_collect_log 11 | // -l the_path_to_output_log 12 | // Collect log from Android logcat: 13 | // 14 | // References: 15 | // - Android logcat usage doc: 16 | // https://developer.android.com/studio/command-line/logcat?hl=zh-cn 17 | // @Author: Mr.Shouheng 18 | //////////////////////////////////////////////////////////////////// 19 | """ 20 | 21 | import os 22 | import sys, getopt 23 | import logging 24 | import signal 25 | import sys 26 | import threading 27 | from time import sleep 28 | from typing import List 29 | sys.path.insert(0, '../') 30 | import global_config 31 | from android.adb import * 32 | from android.am import * 33 | 34 | LOGCAT_COLLECT_MAX_TIME = 10 # 10 minutes 35 | 36 | command_info = "\ 37 | Options: \n\ 38 | -h[--help] Help info\n\ 39 | -f[--forcestop] Should stop living app\n\ 40 | -p[--package] Package name of analysing\n\ 41 | -l[--log] Log file path from logcat\ 42 | " 43 | 44 | def __show_invalid_command(info: str): 45 | '''Show invliad command info.''' 46 | print('Error: Unrecognized command: %s' % info) 47 | print(command_info) 48 | 49 | class CollectInfo: 50 | ''''Collect info object.''' 51 | def __init__(self, package: str, path: str, fc: str): 52 | self.package = package 53 | self.path = path 54 | self.fc = fc 55 | 56 | def __parse_command(argv) -> CollectInfo: 57 | '''Parse collect info from input command.''' 58 | try: 59 | # : and = means accept arguments 60 | opts, args = getopt.getopt(argv, "-h:-p:-l:-f", ["help", "package=", 'log=', 'forcestop=']) 61 | except BaseException as e: 62 | __show_invalid_command(str(e)) 63 | sys.exit(2) 64 | if len(opts) == 0: 65 | __show_invalid_command('empty parameters') 66 | return 67 | package = path = forcestop = None 68 | for opt, arg in opts: 69 | if opt in ('-l', '--log'): 70 | path = arg 71 | elif opt in ('-p', '--package'): 72 | package = arg 73 | elif opt in ('-f', '--forcestop'): 74 | forcestop = arg 75 | elif opt in ('-h', '--help'): 76 | print(command_info) 77 | return 78 | if package == None or path == None: 79 | __show_invalid_command('Package or log output path required!') 80 | return 81 | return CollectInfo(package, path, forcestop) 82 | 83 | def __logcat_redirect(pro: str, pid: str, info: CollectInfo): 84 | '''Do logcat redirect.''' 85 | # Be sure to add the '-d' option, otherwise the logcat won't write to screen. 86 | os.system("adb shell logcat --pid=" + pid + " -d > " + info.path + "/" + info.package + "" + pro + ".log") 87 | 88 | def __kill_collect_process(): 89 | '''Kill current process to stop all logcat collect threads.''' 90 | out = os.popen("ps -ef | grep collect.py | grep -v grep | awk '{print $2}'") 91 | pid = out.read().strip() 92 | logging.debug(">> Collect trying to kill collect process [" + pid + "].") 93 | # os.system("kill -9 " + pid) 94 | os.kill(int(pid), signal.SIGTERM) 95 | 96 | def __execute(argv): 97 | ''''Execute command.''' 98 | # Parse command. 99 | info = __parse_command(argv) 100 | if info == None: 101 | return 102 | # Restart App if necessary. 103 | if info.fc != None: 104 | restart_app_by_package_name(info.package) 105 | logging.info(">> Collect logcat for package [" + info.package + "].") 106 | # Parse all process and their pids 107 | processes = get_processes_of_pcakage_name(info.package) 108 | logging.info(">> Collect process: [" + str(processes) + "].") 109 | threads = [threading.Thread] 110 | for process in processes: 111 | logging.info(">> Collect process: [" + process.pro + "].") 112 | thread = threading.Thread(target=__logcat_redirect, args=(process.pro, process.pid, info)) 113 | thread.name = process.pro + "(" + process.pid + ")" 114 | threads.append(thread) 115 | for thread in threads: 116 | thread.start() 117 | logging.info(">> Collect for process: [" + thread.name + "] started.") 118 | timer = threading.Timer(LOGCAT_COLLECT_MAX_TIME, __kill_collect_process) 119 | timer.start() 120 | 121 | if __name__ == "__main__": 122 | '''Program entrance.''' 123 | global_config.config_logging('../log/app.log') 124 | __execute(sys.argv[1:]) 125 | -------------------------------------------------------------------------------- /initializer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import shutil 5 | import os 6 | import time 7 | import json 8 | import logging 9 | from importer import Importer as Importer 10 | from file_operator import JsonOperator as JsonOperator 11 | from config import REPO_CONFIG_PATH 12 | from config import APP_CONFIG_PATH 13 | from config import APP_LANGUAGE_CONFIG_PATH 14 | from config import REPO_CONFIG_PATH_PREFIX 15 | 16 | # 项目初始化工具类 17 | class Initializer: 18 | # 初始化 19 | def __init__(self): 20 | pass 21 | 22 | # 仓库是否已经初始化 23 | def is_repo_initialized(self): 24 | return os.path.exists(REPO_CONFIG_PATH) 25 | 26 | # 初始化项目 27 | def initialize(self, andoird_dir, ios_dir, support_languages): 28 | logging.debug("[initialize] android dir : " + str(andoird_dir) + " ios dir : " + str(ios_dir) + " support languages : " + str(support_languages)) 29 | # 备份 & 移除配置文件 30 | if os.path.exists(REPO_CONFIG_PATH): 31 | dest_path = REPO_CONFIG_PATH_PREFIX % (time.strftime('%Y-%m-%d %M-%I-%S', time.localtime())) 32 | shutil.copyfile(REPO_CONFIG_PATH, dest_path) 33 | os.remove(REPO_CONFIG_PATH) 34 | # 添加支持的多语言 35 | appConfig.initialize() 36 | appConfig.add_support_languages(support_languages) 37 | appConfig.android_resources_root_directory = andoird_dir 38 | appConfig.ios_resources_root_directory = ios_dir 39 | # 生成多语言仓库文件 40 | importer = Importer(appConfig) 41 | if len(andoird_dir) > 0: 42 | importer.import_android_resources() 43 | if len(ios_dir) > 0: 44 | importer.import_ios_resources() 45 | # 将配置写入到文件中 46 | appConfig.write_to_json() 47 | importer.gen_repo_config() 48 | 49 | # 应用配置 50 | class AppConfig: 51 | # 初始化 52 | def __init__(self): 53 | self.language = "" # 当前使用的多语言 54 | self.support_languages = [] # 支持的语言 55 | self.languages = {} # 所有多语言 56 | self.android_resources_root_directory = "" # Android 多语言资源根目录 57 | self.ios_resources_root_directory = "" # iOS 多语言资源根目录 58 | self.ios_language_black_list = [] # iOS 多语言黑名单,指语言缩写存在于 Android 而不存在于 iOS 中的 59 | self.android_language_black_list = [] # Android 多语言黑名单,指语言缩写存在于 iOS 而不存在于 Android 中的 60 | self.translate_excel_output_directory = "" # 翻译 Excel 导出文件位置 61 | # 加载配置文件 62 | with open(APP_CONFIG_PATH, "r") as f: 63 | config_json = json.load(f) 64 | self.language = config_json["language"] 65 | self.support_languages = config_json["support_languages"] 66 | self.languages = config_json["languages"] 67 | self.android_resources_root_directory = config_json.get("android_resources_root_directory") 68 | self.ios_resources_root_directory = config_json.get("ios_resources_root_directory") 69 | self.ios_language_black_list = config_json.get("ios_language_black_list") 70 | self.android_language_black_list = config_json.get("android_language_black_list") 71 | self.translate_excel_output_directory = config_json.get("translate_excel_output_directory") 72 | 73 | # 初始化应用配置 74 | def initialize(self): 75 | self.support_languages.clear() 76 | 77 | # 添加多语言支持 78 | def add_support_languages(self, support_languages): 79 | for support_language in support_languages: 80 | # 过滤已经存在的多语言 81 | if support_language not in self.support_languages: 82 | self.support_languages.append(support_language) 83 | logging.debug("Added support languages : " + str(self.support_languages)) 84 | 85 | # 将配置写入到 json 文件中 86 | def write_to_json(self): 87 | json_obj = { 88 | "language": self.language, 89 | "support_languages": self.support_languages, 90 | "languages": self.languages, 91 | "ios_language_black_list": self.ios_language_black_list, 92 | "android_language_black_list": self.android_language_black_list, 93 | "ios_resources_root_directory": self.ios_resources_root_directory, 94 | "android_resources_root_directory": self.android_resources_root_directory, 95 | "translate_excel_output_directory": self.translate_excel_output_directory 96 | } 97 | jsonOperator = JsonOperator() 98 | jsonOperator.write_json(APP_CONFIG_PATH, json_obj) 99 | 100 | # 应用多语言工具类,用于翻译工具的多语言 101 | class LanguageFactory: 102 | # 初始化 103 | def __init__(self): 104 | self.entries = {} 105 | 106 | # 加载多语言资源 107 | def load_languages(self): 108 | jsonOperator = JsonOperator() 109 | self.entries = jsonOperator.read_json(APP_LANGUAGE_CONFIG_PATH) 110 | 111 | # 获取多语言词条 112 | def get_entry(self, name): 113 | return self.entries[name][appConfig.language] 114 | 115 | # the singleton app config bean 116 | appConfig = AppConfig() 117 | -------------------------------------------------------------------------------- /repository.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | 5 | import logging 6 | from file_operator import JsonOperator as JsonOperator 7 | from config import REPO_CONFIG_PATH 8 | 9 | # 仓库加载 10 | class Repository: 11 | # 初始化 12 | def __init__(self): 13 | self.datas = [] 14 | self.keywords = [] 15 | self.languages = [] 16 | self.__loaded = False 17 | 18 | # 加载数据仓库 19 | def load(self): 20 | if self.__loaded: 21 | return 22 | jsonOperator = JsonOperator() 23 | self.datas = jsonOperator.read_json(REPO_CONFIG_PATH) 24 | for data in self.datas: 25 | self.keywords.append(data["keyword"]) 26 | if len(self.languages) == 0: 27 | for k, v in data["translates"].items(): 28 | self.languages.append(k) 29 | self.__loaded = True 30 | logging.debug("Loaded keywords : " + str(self.keywords)) 31 | logging.debug("Loaded languages : " + str(self.languages)) 32 | 33 | # 尝试添加新的语言 34 | def try_to_add_new_language(self, language): 35 | if language not in self.languages: 36 | self.languages.append(language) 37 | logging.debug("Tring to add new language " + language + " into " + str(self.datas)) 38 | for data in self.datas: 39 | data["translates"][language] = "" 40 | logging.info("Language added : " + language) 41 | return 42 | logging.info("Language exists : " + language) 43 | 44 | # 尝试添加新的词条 45 | def try_to_add_new_keyword(self, keyword, translation, language): 46 | if keyword not in self.keywords: 47 | # 添加一个新的词条 48 | translations = {} 49 | self.__init_keyword_translations(translation, translations, language) 50 | self.datas.append({"keyword": keyword, "comment": "", "translates": translations}) 51 | self.keywords.append(keyword) 52 | else: 53 | # 判断词条是否发生了变更(之前调用 try_to_add_new_language 的时候处理了新增多语言的情况) 54 | for data in self.datas: 55 | if data["keyword"] == keyword: 56 | old_translation = data["translates"][language] 57 | # 应该过滤掉 old_translation 为空掉情况,此时说明它已经被处理过了,没必要再次处理 58 | # 所以这就意味着不要一次更改两个多语言文件的同一词条,因为我们通过词条变更来确定哪些词条的多语言需要重新翻译, 59 | # 如果同时修改,那么只能以多语言文件列表的第一个文件的改动为准,因此造成结果不可预知 60 | # 建议通过导出 Excel 的方式,通过修改 Excel 并重新导入到项目中的方式来导入自己翻译的多语言 61 | if old_translation != translation and len(old_translation) != 0: 62 | logging.debug("Found translation change for : " + keyword + " (" + language + ").") 63 | self.__init_keyword_translations(translation, data["translates"], language) 64 | # 处理完毕,找到一个就可以结束了 65 | break 66 | 67 | # 修改词条 68 | def try_ro_modify_keyword(self, keyword, translation, language): 69 | # 不存在的语言不支持修改 70 | if language not in self.languages: 71 | return 72 | # 遍历更新 73 | for data in self.datas: 74 | if data["keyword"] == keyword and language in data["translates"]: 75 | if data["translates"][language] != translation: 76 | data["translates"][language] = translation 77 | break 78 | 79 | # 更新多语言词条 80 | def update_keyword(self, keyword, translation, language): 81 | for data in self.datas: 82 | if data["keyword"] == keyword: 83 | if language in data["translates"]: 84 | data["translates"][language] = translation 85 | 86 | # 重新生成 repo json 87 | def rewrite_repo_json(self): 88 | jsonOperator = JsonOperator() 89 | jsonOperator.write_json(REPO_CONFIG_PATH, self.datas) 90 | 91 | # 获取仓库当前的状态 92 | def get_repo_state(self): 93 | self.load() 94 | missed_count = 0 95 | for data in self.datas: 96 | # 对每个词条进行处理 97 | for k, v in data["translates"].items(): 98 | if len(v) == 0: 99 | missed_count = missed_count + 1 100 | break 101 | return {"missed_count": missed_count} 102 | 103 | # 获取用于翻译的多语言 104 | def get_keywords_to_translate(self): 105 | self.load() 106 | dist = {} 107 | for data in self.datas: 108 | keyword = data["keyword"] 109 | to_languages = [] 110 | from_language = "" 111 | from_translation = "" 112 | for k,v in data["translates"].items(): 113 | if len(v) == 0: 114 | to_languages.append(k) 115 | else: 116 | from_language = k 117 | from_translation = v 118 | dist[keyword] = [] 119 | for language in to_languages: 120 | dist[keyword].append({"translation":from_translation, "from":from_language, "to":language}) 121 | return dist 122 | 123 | # 初始化词条的多语言 124 | def __init_keyword_translations(self, translation, translations, language): 125 | for l in self.languages: 126 | if l == language: 127 | translations[l] = translation 128 | else: 129 | translations[l] = "" 130 | 131 | # The singleton repository 132 | repository = Repository() 133 | -------------------------------------------------------------------------------- /android/adb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from typing import List 6 | 7 | # //////////////////////////////////////////////////////////////////// 8 | # 9 | # The Android Debug Bridge Wrapper Project by Python 10 | # 11 | # @Author Mr.Shouheng 12 | # 13 | # @References: 14 | # - https://developer.android.com/studio/command-line/adb?hl=zh-cn#am 15 | # 16 | # //////////////////////////////////////////////////////////////////// 17 | 18 | class DeviceInfo: 19 | '''Adb attached device info.''' 20 | def __init__(self) -> None: 21 | self.infos = [] 22 | 23 | def append(self, info: str): 24 | '''Append one info to infos.''' 25 | self.infos.append(info) 26 | 27 | def __str__(self) -> str: 28 | ret = '' 29 | for info in self.infos: 30 | ret = ret + info + '\n' 31 | return ret 32 | 33 | def get_devices() -> List[DeviceInfo]: 34 | '''Get all attached devices.''' 35 | text = os_popen("adb devices -l", None) 36 | lines = [line.strip() for line in text.split('\n')] 37 | devices = [] 38 | for line in lines[1:]: 39 | parts = [part.strip() for part in line.split(' ')] 40 | device = DeviceInfo() 41 | index = 0 42 | for part in parts: 43 | if len(part) > 0: 44 | index = index+1 45 | if index == 1: 46 | device.append("serial:" + part) 47 | elif index == 2: 48 | device.append("state:" + part) 49 | else: 50 | device.append(part) 51 | devices.append(device) 52 | return devices 53 | 54 | def get_version() -> List[str]: 55 | '''Get adb version info.''' 56 | text = os_popen("adb version", None) 57 | lines = [line.strip() for line in text.split('\n')] 58 | return lines 59 | 60 | def reboot(serial: str = None) -> int: 61 | '''Reboot device.''' 62 | return os_system("adb %s reboot ", serial) 63 | 64 | def export_bugreport(path: str, serial: str = None) -> int: 65 | '''Export bugreport, ANR log etc.''' 66 | return os_system("adb %s bugreport %s", serial, path) 67 | 68 | def install(apk: str, serial: str = None) -> int: 69 | ''' 70 | Install given package: 71 | - apk: path of APK. 72 | ''' 73 | return os_system("adb %s install %s ", serial, apk) 74 | 75 | def uninstall(pkg: str, keep: bool, serial: str = None) -> bool: 76 | ''' 77 | Uninstall given package. 78 | - pkg: package name 79 | - keep: keep the data and cache directories if true, otherwise false. 80 | ''' 81 | if keep: 82 | return os_system("adb %s uninstall -k %s ", serial, pkg) == 0 83 | else: 84 | return os_system("adb %s uninstall %s ", serial, pkg) == 0 85 | 86 | def push(local: str, remote: str, serial: str = None) -> int: 87 | ''' 88 | Push local file to remote directory. 89 | - local: local file path 90 | - remote: the remote path to push to 91 | ''' 92 | return os_system("adb %s push %s %s", serial, local, remote) 93 | 94 | def pull(remote: str, local: str, serial: str = None) -> int: 95 | ''' 96 | Pull file from remote phone: 97 | - remote: remote file path 98 | - local: local to store file 99 | ''' 100 | return os_system("adb %s pull %s %s", serial, remote, local) 101 | 102 | def os_system(command: str, serial: str, *args) -> int: 103 | '''Execute command by os.system().''' 104 | f_args = [] 105 | if serial is None: 106 | f_args.append(" ") 107 | else: 108 | f_args.append(" -s %s " % serial) 109 | f_args.extend(args) 110 | if len(f_args) == 1 and f_args[0] == " ": 111 | return os.system(command) 112 | return os.system(command % tuple(f_args)) 113 | 114 | def os_popen(command: str, serial: str, *args) -> str: 115 | '''Execute command by os.popen().''' 116 | f_args = [] 117 | if serial is None: 118 | f_args.append(" ") 119 | else: 120 | f_args.append(" -s %s " % serial) 121 | f_args.extend(args) 122 | if len(f_args) == 1 and f_args[0] == " ": 123 | out = os.popen(command) 124 | else: 125 | out = os.popen(command % tuple(f_args)) 126 | return out.read().strip() 127 | 128 | def print_list(list: List): 129 | '''Print list.''' 130 | for item in list: 131 | print(str(item) + ", ") 132 | 133 | if __name__ == "__main__": 134 | TEST_PACKAGE_NAME = "me.shouheng.leafnote" 135 | TEST_DEVICE_SERIAL = "emulator-5556" 136 | TEST_APK_PATH = "/Users/wangshouheng/downloads/Snapdrop-0.3.apk" 137 | TEST_LOCAL_FILE = "adb.py" 138 | TEST_REMOTE_DIR = "/sdcard" 139 | TEST_REMOTE_FILE = "/sdcard/adb.py" 140 | # print_list(get_devices()) 141 | # print(get_version()) 142 | # print(reboot()) 143 | # print(reboot(TEST_DEVICE_SERIAL)) 144 | # print(export_bugreport('~/bugs')) 145 | # print(export_bugreport('~/bugs', TEST_DEVICE_SERIAL)) 146 | # print(uninstall(TEST_PACKAGE_NAME, False)) 147 | # print(uninstall(TEST_PACKAGE_NAME, False, TEST_DEVICE_SERIAL)) 148 | # print(install(TEST_APK_PATH)) 149 | # print(install(TEST_APK_PATH, TEST_DEVICE_SERIAL)) 150 | # print(os_system("ls -al " + TEST_APK_PATH, None)) 151 | # print(push(TEST_LOCAL_FILE, TEST_REMOTE_DIR, TEST_DEVICE_SERIAL)) 152 | # print(pull(TEST_REMOTE_FILE, "~", TEST_DEVICE_SERIAL)) 153 | # os_system("a", "b", "c", "d", "e") 154 | pull("/data/local/tmp/temp_sampling.trace", ".", TEST_DEVICE_SERIAL) 155 | -------------------------------------------------------------------------------- /translator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | import hashlib 6 | import logging 7 | import json 8 | import time 9 | from config import API_URL 10 | from config import BAIDU_CONFIG_URL 11 | from repository import repository 12 | 13 | # 百度翻译工具类 14 | class BaiduTranslator: 15 | # 初始化 16 | def __init__(self): 17 | self.mappings = {} # 语言映射表 18 | self.reversed_mappings = {} # 反向语言映射表 19 | self.appid = "" # 文字识别 AppId 20 | self.secret = "" # 文字识别 App Secret 21 | self.__load_configs() # 加载配置文件 22 | 23 | # 将指定的字符串翻译为指定的语言(传入的 language 应当是 App 多语言缩写) 24 | def translate(self, words, language): 25 | try: 26 | mapped_language = self.__get_mapped_language(language) 27 | if mapped_language == None: 28 | logging.warning("Missed language mapping \"" + language + "\"") 29 | return "" 30 | translated = self.translate_directly(words, mapped_language) 31 | # 返回一个字典,包含翻译结果和映射的多语言 32 | return {"translation":translated, "mapped_language":mapped_language} 33 | except KeyError as ke: 34 | logging.error("Unknown language " + language + " : " + ke) 35 | return "" 36 | 37 | # 将指定的字符串翻译为指定的语言(传入的 mapped_language 就是 baidu 平台定义的语言缩写) 38 | def translate_directly(self, words, mapped_language): 39 | logging.info("Translate \"" + words + "\" to \"" + mapped_language + "\".") 40 | form_data = self.__get_post_form_data(words, mapped_language) 41 | # 增加一点延时 42 | time.sleep(1) 43 | response = requests.post(API_URL, form_data) 44 | result = response.json() 45 | try: 46 | translated = result["trans_result"][0]["dst"] 47 | logging.info("Translated \"" + words + "\" to \"" + translated + "\".") 48 | return translated 49 | except KeyError: 50 | logging.error("Translate error " + result["error_code"] + " " + result["error_msg"]) 51 | return "" 52 | 53 | # 获取方向的语言映射列表,比如 香港和澳门的繁体都是繁体,此时可以用这个映射,避免重复翻译 54 | def get_reversed_mapped_languages(self, from_language): 55 | return self.reversed_mappings.get(from_language) 56 | 57 | # 判断是否配置完成 58 | def is_configed(self): 59 | return self.appid != "your_app_id" and self.secret != "your_app_secret" 60 | 61 | # 获取指定的语言对应的 Baidu 翻译语言参数 62 | def __get_mapped_language(self, language): 63 | return self.mappings.get(language) 64 | 65 | # 加载配置文件 66 | def __load_configs(self): 67 | with open(BAIDU_CONFIG_URL) as fp: 68 | data = json.load(fp) 69 | logging.debug(data["mappings"]) 70 | self.mappings = data["mappings"] 71 | self.appid = data["app_id"] 72 | self.secret = data["app_secret"] 73 | # 构建反向的多语言映射 74 | for k,v in self.mappings.items(): 75 | if v not in self.reversed_mappings: 76 | self.reversed_mappings[v] = [] 77 | self.reversed_mappings[v].append(k) 78 | 79 | # 获取用于发送的 post form 数据 80 | def __get_post_form_data(self, words, language): 81 | dist = {} 82 | dist['q'] = words 83 | dist['from'] = 'auto' 84 | dist['to'] = language 85 | dist['appid'] = self.appid 86 | salt = '12321321' 87 | dist['salt'] = salt 88 | sign = self.appid + words + salt + self.secret 89 | hl = hashlib.md5() 90 | hl.update(sign.encode(encoding='utf-8')) 91 | md5_result = hl.hexdigest() 92 | dist['sign'] = md5_result 93 | return dist 94 | 95 | # 翻译工具类 96 | class Translator: 97 | # 初始化 98 | def __init__(self): 99 | pass 100 | 101 | # 开始翻译 102 | def start_translate(self, progress_callback, finish_callback): 103 | repository.load() 104 | translator = BaiduTranslator() 105 | dist = repository.get_keywords_to_translate() 106 | logging.debug("Dist to translate : " + str(dist)) 107 | total = len(dist) 108 | count = 0 109 | # 遍历词条 110 | translated_languages = [] 111 | for keyword,v in dist.items(): 112 | logging.debug("Translating " + keyword) 113 | # 清空标记 114 | translated_languages.clear() 115 | for item in v: 116 | # from_language = item["from"] 117 | to_language = item["to"] 118 | translation = item["translation"] 119 | if to_language in translated_languages: 120 | continue 121 | # 可能已经被翻译过了 122 | result = translator.translate(translation, to_language) 123 | if len(result) != 0: 124 | translated = result["translation"] 125 | mapped_language = result["mapped_language"] 126 | # 反向的映射关系填充翻译结果 127 | reversed_mappings = translator.get_reversed_mapped_languages(mapped_language) 128 | logging.debug("Get reversed mappings for " + mapped_language + " : " + str(reversed_mappings)) 129 | for reversed_mapping in reversed_mappings: 130 | # 标记方向映射列表,更新词条信息 131 | translated_languages.append(reversed_mapping) 132 | repository.update_keyword(keyword, translated, reversed_mapping) 133 | else: 134 | logging.warning("One keyword missed to translate : " + keyword) 135 | # 回调 136 | count = count + 1 137 | progress_callback(count * 100 / total) 138 | # 重写 repo json 139 | repository.rewrite_repo_json() 140 | finish_callback() 141 | -------------------------------------------------------------------------------- /file_operator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from xml.dom.minidom import parse 5 | import xml.dom.minidom 6 | import logging 7 | import json 8 | import xlwt 9 | import os 10 | import codecs 11 | import xlrd 12 | from config import TRANSLATE_EXCEL_SHEET_NAME 13 | 14 | # Xml 操作类 15 | class XmlOperator: 16 | # 初始化 17 | def __init__(self): 18 | pass 19 | 20 | # 读取 Android 的 xml 资源(当前只支持读取 string 标签) 21 | def read_android_resources(self, fname): 22 | logging.debug("Reading Android resources of \"" + fname + "\"") 23 | # 使用 minidom 解析器打开 XML 文档 24 | dist = {} 25 | try: 26 | DOMTree = xml.dom.minidom.parse(fname) 27 | except Exception: 28 | logging.error("Failed to read Android resources : " + fname) 29 | return dist 30 | collection = DOMTree.documentElement 31 | # 读取所有 string 标签 32 | strings = collection.getElementsByTagName("string") 33 | # 将读取到的所有元素拼接成字典列表 34 | index = 0 35 | for string in strings: 36 | index += 1 37 | try: 38 | keyword = string.getAttribute('name') 39 | translation = "" 40 | if len(string.childNodes) == 1: 41 | node = string.childNodes[0] 42 | # CDATA 特殊处理 43 | if node.nodeType == 4: 44 | translation = "" 45 | else: 46 | translation = node.data 47 | else: 48 | # 对子节点遍历,将所有对子节点拼接起来 49 | for node in string.childNodes: 50 | # CDATA 特殊处理 51 | if node.nodeType == 4: 52 | translation = translation + "" 53 | else: 54 | translation = translation + node.toxml() 55 | # 过滤处理 56 | if '\\\'' in translation: 57 | translation = translation.replace('\\\'', '\'') 58 | dist[keyword] = translation 59 | except BaseException as e: 60 | logging.error("Invalid entry at index " + str(index) + " " + str(keyword) + " : " + str(e)) 61 | # 返回解析结果 62 | logging.debug("Read Android resources \"" + fname + "\" end.") 63 | return dist 64 | 65 | # 将 Android 的 xml 资源写入到文件中 66 | def write_android_resources(self, dist, fname): 67 | logging.debug("Writing Android resources " + fname + " : " + str(dist)) 68 | content = '\n' 69 | for k, v in dist.items(): 70 | # 处理 ' 71 | if '\'' in v: 72 | v = v.replace("\'", "\\\'") 73 | # 处理 > 和 < 74 | if ('>' in v or '<' in v) and '', '>') 76 | v = v.replace('<', '<') 77 | # 处理 … 78 | if '…' in v and '' + v + '\n' 82 | content += '' 83 | with open(fname, 'w', encoding='utf-8') as f: 84 | f.write(content) 85 | 86 | # Json 操作类 87 | class JsonOperator: 88 | # 初始化 89 | def __init__(self): 90 | pass 91 | 92 | # 写入 json 到文件 93 | def write_json(self, fname, json_obj): 94 | json_str = json.dumps(json_obj) 95 | with open(fname, "w") as f: 96 | f.write(json_str) 97 | 98 | # 从文件读取 json 字符串 99 | def read_json(self, fname): 100 | with open(fname, "r") as f: 101 | return json.load(f) 102 | 103 | # 文件操作类,用于 iOS 文件操作 104 | class FileOperator: 105 | # 初始化 106 | def __init__(self): 107 | pass 108 | 109 | # 读取 iOS 词条 110 | def read_ios_keywords(self, fname): 111 | logging.debug("Reading ios keywrods : " + fname) 112 | dist = {} 113 | with open(fname, 'r') as f: 114 | # 读取每一行的数据 115 | ls = [line.strip() for line in f] 116 | f.close() 117 | for l in ls: 118 | sps = l.split("=") 119 | keyword = sps[0].strip()[1:-1] 120 | translation = sps[1].strip()[1:-2] 121 | dist[keyword] = translation 122 | return dist 123 | 124 | # 生成 iOS 的多语言资源 125 | def write_ios_resources(self, dist, fname): 126 | logging.debug("Writing iOS resources " + fname + " : " + str(dist)) 127 | content = '' 128 | for k, v in dist.items(): 129 | content = content + "\"" + k + "\" = \"" + v + "\";\n" 130 | with open(fname, 'w', encoding='utf-8') as f: 131 | f.write(content) 132 | 133 | # Excel 操作类 134 | class ExcelOperator: 135 | # 初始化 136 | def __init__(self): 137 | pass 138 | 139 | # 写 Excel 140 | # TODO 这里到 sheet name 应该从 dist 中读取,下面这样不具备通用性 141 | def write_excel(self, dist, file): 142 | # 创建 Excel 的工作簿 143 | book = xlwt.Workbook(encoding='utf-8', style_compression=0) 144 | sheet = book.add_sheet(TRANSLATE_EXCEL_SHEET_NAME, cell_overwrite_ok=True) 145 | # dist : {"a":[], "b":[], "c": []} 146 | row_count = 0 147 | col_count = 0 148 | for k, v in dist.items(): 149 | sheet.write(row_count, col_count, k) 150 | for item in v: 151 | row_count = row_count + 1 152 | sheet.write(row_count, col_count, item) 153 | col_count = col_count + 1 154 | row_count = 0 155 | book.save(file) 156 | 157 | # 读取 Excel,{sheet_name:[[col 1], [col 2], []]} 158 | def read_excel(self, xlsfile): 159 | dists = {} 160 | book = xlrd.open_workbook(xlsfile) 161 | size = len(book.sheet_names()) 162 | for i in range(size): 163 | sheet = book.sheet_by_index(i) 164 | dists[sheet.name] = self.__read_sheet(sheet) 165 | return dists 166 | 167 | # 读取 Excel Sheet 168 | def __read_sheet(self, sheet): 169 | col_list = [] 170 | # 按列遍历 171 | for col in range(0, sheet.ncols): 172 | col_list.append([]) 173 | # 按行遍历 174 | for row in range(0, sheet.nrows): 175 | value = sheet.cell_value(row, col) 176 | col_list[col].append(value) 177 | return col_list 178 | -------------------------------------------------------------------------------- /android/am.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # //////////////////////////////////////////////////////////////////// 5 | # 6 | # The Android Debug Bridge Wrapper Project by Python 7 | # 8 | # @Author Mr.Shouheng 9 | # 10 | # @References: 11 | # - https://developer.android.com/studio/command-line/adb?hl=zh-cn#am 12 | # 13 | # //////////////////////////////////////////////////////////////////// 14 | 15 | import logging 16 | from time import sleep 17 | from typing import List 18 | from adb import * 19 | import sys 20 | sys.path.insert(0, '../') 21 | import global_config 22 | 23 | class ProcessInfo: 24 | '''Process information wrapper object.''' 25 | def __init__(self, pro: str, pid: str): 26 | self.pro = pro 27 | self.pid = pid 28 | 29 | def __str__(self) -> str: 30 | return "%s(%s)" % (self.pro, self.pid) 31 | 32 | def stop_app_by_package_name(pkg: str, serial: str = None) -> bool: 33 | '''Stop App of given package name.''' 34 | return os_system("adb %s shell am force-stop %s ", serial, pkg) == 0 35 | 36 | def get_launcher_of_package_name(pkg: str, serial: str = None) -> str: 37 | '''Get launcher class of given package name.''' 38 | out = os_popen("adb %s shell dumpsys package %s ", serial, pkg) 39 | index = out.find('Category: "android.intent.category.LAUNCHER"') 40 | part = out[0:index] 41 | s_index = part.rfind(pkg) 42 | part = part[s_index:index] 43 | e_index = part.find(' ') 44 | launcher = part[0:e_index] 45 | return launcher 46 | 47 | def start_app_by_package_name(pkg: str, serial: str = None) -> bool: 48 | '''Start App by launcher class.''' 49 | launcher = get_launcher_of_package_name(pkg, serial) 50 | return os_system("adb %s shell am start %s ", serial, launcher) == 0 51 | 52 | def restart_app_by_package_with_profiling(pkg: str, file: str, serial: str = None) -> int: 53 | '''Restart APP with given package name.''' 54 | stop_app_by_package_name(pkg, serial) 55 | launcher = get_launcher_of_package_name(pkg, serial) 56 | ret = os_popen("adb %s shell am start -n %s --start-profiler %s --sampling %s ", serial, launcher, file, '10') 57 | logging.debug("Start info: " + ret) 58 | return 1 59 | 60 | def stop_profiler_and_download_trace_file(pkg: str, file: str, to: str, serial: str = None) -> int: 61 | '''Stop profiler and download the trace file to local.''' 62 | pros = get_processes_of_pcakage_name(pkg, serial) 63 | logging.debug("Try to kill process : " + str(pros[0])) 64 | out = os_popen("adb %s shell am profile stop %s ", serial, pros[0].pid) 65 | logging.debug("Stop profiler: " + out) 66 | sleep(10) # Delays for few seconds, currently may writing to the profiler file. (It's emprt now.) 67 | ret = pull(file, to, serial) 68 | logging.debug("Pull file from %s to %s : %s" % (file, to, str(ret))) 69 | return ret 70 | 71 | def restart_app_by_with_profiling_and_duration(pkg: str, to: str, duration: int, serial: str = None) -> int: 72 | ''' 73 | Restart APP and profiler by package name. 74 | - pkg: package name 75 | - to: local file path to store the profiler file 76 | - duration: the time duration to collect the profiler data 77 | ''' 78 | smapling_file = "/data/local/tmp/temp_sampling.trace" 79 | restart_app_by_package_with_profiling(pkg, smapling_file, serial) 80 | sleep(duration) 81 | return stop_profiler_and_download_trace_file(pkg, smapling_file, to, serial) 82 | 83 | def dumpheap_by_package(pkg: str, to: str, serial: str = None) -> int: 84 | '''Dumpheap of given package.''' 85 | pros = get_processes_of_pcakage_name(pkg, serial) 86 | temp = "/data/local/tmp/temp.heap" 87 | if len(pros) == 0: 88 | return -1 89 | logging.debug("Try to dump process : " + str(pros[0])) 90 | os_system("adb %s shell am dumpheap %s %s ", serial, pros[0].pid, temp) 91 | return pull(temp, to, serial) 92 | 93 | def dumpheap_by_package(pkg: str, process_name: str, to: str, serial: str = None) -> int: 94 | '''Dumpheap of given package.''' 95 | pros = get_processes_of_pcakage_name(pkg, serial) 96 | temp = "/data/local/tmp/temp.heap" 97 | if len(pros) == 0: 98 | return -1 99 | pro = pros[0] 100 | for p in pros: 101 | if p == process_name: 102 | pro = p 103 | logging.debug("Try to dump process : " + str(pro)) 104 | os_system("adb %s shell am dumpheap %s %s ", serial, pro.pid, temp) 105 | return pull(temp, to, serial) 106 | 107 | def restart_app_by_package_name(pkg: str, serial: str = None) -> bool: 108 | '''Restart App by package name.''' 109 | stop_app_by_package_name(pkg, serial) 110 | return start_app_by_package_name(pkg, serial) 111 | 112 | def get_processes_of_pcakage_name(pkg: str, serial: str = None) -> List[ProcessInfo]: 113 | '''Get all processes info of package name.''' 114 | processes = [] 115 | # The 'awk' command is invlaid on adb shell, so it will take into effect. 116 | text = os_popen("adb %s shell \"ps -ef|grep %s|grep -v grep| awk \'{print $1}\'\"", serial, pkg) 117 | lines = [line.strip() for line in text.split('\n')] 118 | for line in lines: 119 | parts = [col.strip() for col in line.split(' ')] 120 | cols = [] 121 | for part in parts: 122 | if part != '': 123 | cols.append(part) 124 | if len(cols) == 0: 125 | continue 126 | pid = cols[1] 127 | pro = cols[7].removeprefix(pkg) 128 | if pro == '': 129 | pro = ':main' 130 | process = ProcessInfo(pro, pid) 131 | processes.append(process) 132 | return processes 133 | 134 | def get_top_activity_info(serial: str = None) -> str: 135 | '''Get top activities info.''' 136 | return os_popen("adb %s shell dumpsys activity top", serial) 137 | 138 | def capture_screen(to: str, serial: str = None) -> int: 139 | '''Capture screen.''' 140 | temp = "/sdcard/screen.png" 141 | os_system("adb %s shell screencap %s ", serial, temp) 142 | return pull(temp, to, serial) 143 | 144 | if __name__ == "__main__": 145 | global_config.config_logging('../log/app.log') 146 | TEST_PACKAGE_NAME = "" 147 | TEST_DEVICE_SERIAL = "emulator-5556" 148 | TEST_APK_PATH = "/Users/wangshouheng/downloads/Snapdrop-0.3.apk" 149 | TEST_LOCAL_FILE = "adb.py" 150 | TEST_REMOTE_DIR = "/sdcard" 151 | TEST_REMOTE_FILE = "/sdcard/adb.py" 152 | # stop_app_by_package_name(TEST_PACKAGE_NAME) 153 | # stop_app_by_package_name(TEST_PACKAGE_NAME, TEST_DEVICE_SERIAL) 154 | # print(get_launcher_of_package_name(TEST_PACKAGE_NAME)) 155 | # print(get_launcher_of_package_name(TEST_PACKAGE_NAME, TEST_DEVICE_SERIAL)) 156 | # print(start_app_by_package_name(TEST_PACKAGE_NAME)) 157 | # print(start_app_by_package_name(TEST_PACKAGE_NAME, TEST_DEVICE_SERIAL)) 158 | # print_list(get_processes_of_pcakage_name(TEST_PACKAGE_NAME)) 159 | # print_list(get_processes_of_pcakage_name(TEST_PACKAGE_NAME, TEST_DEVICE_SERIAL)) 160 | # print(get_top_activity_info()) 161 | # print(get_top_activity_info(TEST_DEVICE_SERIAL)) 162 | # os_system("adb %s shell am > ./am.md", TEST_DEVICE_SERIAL) 163 | # restart_app_by_with_profiling_and_duration(TEST_PACKAGE_NAME, "~/desktop/sampling.trace", 6, TEST_DEVICE_SERIAL) 164 | # dumpheap_by_package(TEST_PACKAGE_NAME, ":main", "~/desktop/file.heap", TEST_DEVICE_SERIAL) 165 | # capture_screen("~/desktop/screen.png") 166 | -------------------------------------------------------------------------------- /importer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import logging 6 | import json 7 | from file_operator import ExcelOperator as ExcelOperator 8 | from file_operator import XmlOperator as XmlOperator 9 | from file_operator import JsonOperator as JsonOperator 10 | from file_operator import FileOperator as FileOperator 11 | from repository import repository as repository 12 | from config import REPO_CONFIG_PATH 13 | from config import TRANSLATE_EXCEL_SHEET_NAME 14 | 15 | # 导入工具 16 | class Importer: 17 | # 初始化 18 | def __init__(self, appConfig): 19 | self.appConfig = appConfig 20 | self.support_languages = [] # 需要支持的语言 21 | self.keywords = [] # 关键字 22 | self.translates = {} # 翻译的词条 23 | self.entries = [] # 最终得到的翻译条目 24 | 25 | # 项目初始化的时候,导入 Android 翻译资源(values 文件夹根目录) 26 | def import_android_resources(self): 27 | # 遍历解析 Android 资源目录 28 | xmlOperator = XmlOperator() 29 | # 解析所有的多语言 30 | for f in os.listdir(self.appConfig.android_resources_root_directory): 31 | language = self.__get_android_file_language(f) 32 | if len(language) <= 0: 33 | continue 34 | # 语言名称 35 | self.support_languages.append(language) 36 | # 解析多语言的词条 37 | for f in os.listdir(self.appConfig.android_resources_root_directory): 38 | language = self.__get_android_file_language(f) 39 | if len(language) <= 0: 40 | continue 41 | path = os.path.join(self.appConfig.android_resources_root_directory, f, "strings.xml") 42 | dist = xmlOperator.read_android_resources(path) 43 | for k, v in dist.items(): 44 | if k not in self.keywords: 45 | self.keywords.append(k) 46 | if k not in self.translates: 47 | self.translates[k] = {} 48 | for support_language in self.support_languages: 49 | if support_language != language: 50 | self.translates[k][support_language] = "" 51 | else: 52 | self.translates[k][support_language] = v 53 | # 新增多语言的情况:要为应用初始化的时候选中的多语言设置词条 54 | for sl in self.appConfig.support_languages: 55 | if sl not in self.support_languages: 56 | for k, v in self.translates.items(): 57 | self.translates[k][sl] = "" 58 | # 输出用于调试的日志 59 | self.appConfig.add_support_languages(self.support_languages) 60 | logging.debug("Parsed From Android Resources : " + str(self.support_languages)) 61 | logging.debug("Parsed Keywords : " + str(self.keywords)) 62 | logging.debug(self.translates) 63 | 64 | # 项目初始化的时候,导入 iOS 翻译资源(lproj 文件夹根目录) 65 | def import_ios_resources(self): 66 | fileOperator = FileOperator() 67 | # 解析所有的多语言 68 | for f in os.listdir(self.appConfig.ios_resources_root_directory): 69 | language = self.__get_ios_file_language(f) 70 | if len(language) <= 0: 71 | continue 72 | # 语言名称 73 | self.support_languages.append(language) 74 | # 解析多语言的词条 75 | for f in os.listdir(self.appConfig.ios_resources_root_directory): 76 | language = self.__get_ios_file_language(f) 77 | if len(language) <= 0: 78 | continue 79 | path = os.path.join(self.appConfig.ios_resources_root_directory, f, "Localizable.strings") 80 | dist = fileOperator.read_ios_keywords(path) 81 | logging.debug("Read iOS keywords : " + str(dist)) 82 | for k, v in dist.items(): 83 | if k not in self.keywords: 84 | self.keywords.append(k) 85 | if k not in self.translates: 86 | self.translates[k] = {} 87 | for support_language in self.support_languages: 88 | if support_language != language: 89 | self.translates[k][support_language] = "" 90 | else: 91 | self.translates[k][support_language] = v 92 | # 新增多语言的情况:要为应用初始化的时候选中的多语言设置词条 93 | for sl in self.appConfig.support_languages: 94 | if sl not in self.support_languages: 95 | for k, v in self.translates.items(): 96 | self.translates[k][sl] = "" 97 | # 输出用于调试的日志 98 | self.appConfig.add_support_languages(self.support_languages) 99 | logging.debug("Parsed From iOS Resources : " + str(self.support_languages)) 100 | logging.debug("Parsed Keywords : " + str(self.keywords)) 101 | logging.debug(self.translates) 102 | 103 | # 生成多语言仓库配置文件 104 | def gen_repo_config(self): 105 | json_obj = [] 106 | for k, v in self.translates.items(): 107 | json_obj.append({"keyword":k, "comment":"", "translates": v}) 108 | logging.debug(json_obj) 109 | jsonOperator = JsonOperator() 110 | jsonOperator.write_json(REPO_CONFIG_PATH, json_obj) 111 | 112 | # 导入翻译 excel 文件,将翻译结果按照 keyword 写入配置文件 113 | def import_translated_excel(self, fname): 114 | # 读取 Excel 115 | excelOperator = ExcelOperator() 116 | dists = excelOperator.read_excel(fname) 117 | if TRANSLATE_EXCEL_SHEET_NAME in dists: 118 | # 加载仓库 119 | repository.load() 120 | col_list = dists.get(TRANSLATE_EXCEL_SHEET_NAME) 121 | # 获取所有的关键字 122 | keywords = [] 123 | for row in range(1, len(col_list[0])): 124 | keyword = col_list[0][row] 125 | keywords.append(keyword) 126 | # 语言到词条映射 127 | for col in range(1, len(col_list)): 128 | language = "" 129 | translations = [] 130 | for row in range(0, len(col_list[col])): 131 | value = col_list[col][row] 132 | if row == 0: 133 | language = value 134 | else: 135 | translations.append(value) 136 | # 词条对应起来 137 | for j in range(0, len(keywords)): 138 | keyword = keywords[j] 139 | translation = translations[j] 140 | # 更新到仓库 141 | repository.update_keyword(keyword, translation, language) 142 | # 重写 repo json 143 | repository.rewrite_repo_json() 144 | logging.debug(dists) 145 | 146 | # 比较改动的文件 147 | def update_ios_resource(self): 148 | repository.load() 149 | fileOperator = FileOperator() 150 | # 更新多语言 151 | for f in os.listdir(self.appConfig.ios_resources_root_directory): 152 | language = self.__get_ios_file_language(f) 153 | if len(language) <= 0: 154 | continue 155 | # 语言名称 156 | repository.try_to_add_new_language(language) 157 | # 更新词条 158 | for f in os.listdir(self.appConfig.ios_resources_root_directory): 159 | language = self.__get_ios_file_language(f) 160 | if len(language) <= 0: 161 | continue 162 | path = os.path.join(self.appConfig.ios_resources_root_directory, f, "Localizable.strings") 163 | dist = fileOperator.read_ios_keywords(path) 164 | for k, v in dist.items(): 165 | repository.try_to_add_new_keyword(k, v, language) 166 | # 重写 repo json 167 | repository.rewrite_repo_json() 168 | 169 | # 比较改动的文件 170 | def update_android_resource(self): 171 | repository.load() 172 | # 解析指定的多语言路径中的词条 173 | xmlOperator = XmlOperator() 174 | # 更新多语言 175 | for f in os.listdir(self.appConfig.android_resources_root_directory): 176 | language = self.__get_android_file_language(f) 177 | if len(language) <= 0: 178 | continue 179 | # 语言名称 180 | repository.try_to_add_new_language(language) 181 | # 更新词条 182 | for f in os.listdir(self.appConfig.android_resources_root_directory): 183 | language = self.__get_android_file_language(f) 184 | if len(language) <= 0: 185 | continue 186 | path = os.path.join(self.appConfig.android_resources_root_directory, f, "strings.xml") 187 | # 词条新增 or 变更 188 | dist = xmlOperator.read_android_resources(path) 189 | for k, v in dist.items(): 190 | repository.try_to_add_new_keyword(k, v, language) 191 | # 重写 repo json 192 | repository.rewrite_repo_json() 193 | 194 | # 将 iOS 多语言资源修改结果同步到仓库 195 | def import_modified_ios_resource(self): 196 | repository.load() 197 | fileOperator = FileOperator() 198 | # 更新词条 199 | for f in os.listdir(self.appConfig.ios_resources_root_directory): 200 | language = self.__get_ios_file_language(f) 201 | if len(language) <= 0: 202 | continue 203 | path = os.path.join(self.appConfig.ios_resources_root_directory, f, "Localizable.strings") 204 | dist = fileOperator.read_ios_keywords(path) 205 | for k, v in dist.items(): 206 | repository.try_ro_modify_keyword(k, v, language) 207 | # 重写 repo json 208 | repository.rewrite_repo_json() 209 | 210 | # 将 Android 多语言资源修改结果同步到仓库 211 | def import_modified_android_resource(self): 212 | repository.load() 213 | xmlOperator = XmlOperator() 214 | # 更新词条 215 | for f in os.listdir(self.appConfig.android_resources_root_directory): 216 | language = self.__get_android_file_language(f) 217 | if len(language) <= 0: 218 | continue 219 | path = os.path.join(self.appConfig.android_resources_root_directory, f, "strings.xml") 220 | # 词条新增 or 变更 221 | dist = xmlOperator.read_android_resources(path) 222 | for k, v in dist.items(): 223 | repository.try_ro_modify_keyword(k, v, language) 224 | # 重写 repo json 225 | repository.rewrite_repo_json() 226 | 227 | # Android:从文件名中获取文件的多语言 228 | def __get_android_file_language(self, f): 229 | language = "" 230 | if len(f) > 7: 231 | language = f[7:] # values-xx 232 | else: 233 | language = "default" 234 | # 读取 xml 内容 235 | path = os.path.join(self.appConfig.android_resources_root_directory, f, "strings.xml") 236 | if not os.path.exists(path): 237 | logging.error("Language file not found : " + path) 238 | language = "" 239 | return language 240 | 241 | # iOS:从文件名中获取文件的多语言 242 | def __get_ios_file_language(self, f): 243 | language = "" 244 | if len(f) > 5: 245 | language = f[0:-6] 246 | else: 247 | language = "default" 248 | path = os.path.join(self.appConfig.ios_resources_root_directory, f, "Localizable.strings") 249 | if not os.path.exists(path): 250 | logging.error("Language file not found : " + path) 251 | language = "" 252 | return language 253 | -------------------------------------------------------------------------------- /app_gui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from tkinter import * 5 | import logging 6 | import os 7 | import threading 8 | from tkinter.filedialog import askdirectory 9 | from tkinter.messagebox import askokcancel 10 | from tkinter.filedialog import askopenfilename 11 | from tkinter.messagebox import showinfo 12 | from tkinter.messagebox import showwarning 13 | from initializer import appConfig as appConfig 14 | from repository import repository as repository 15 | from initializer import Initializer 16 | from importer import Importer as Importer 17 | from generator import Generator as Generator 18 | from translator import Translator as Translator 19 | from translator import BaiduTranslator as BaiduTranslator 20 | 21 | # 仓库初始化对话框 22 | class RepoInitDialog(Frame): 23 | # 初始化 24 | def __init__(self, root): 25 | self.root = root 26 | self.android_resources_dir = StringVar() 27 | self.ios_resources_dir = StringVar() 28 | self.support_languages = {} 29 | Frame.__init__(self, root) 30 | # 多语言资源根目录选择 31 | fileFrame = Frame(root) 32 | fileFrame.pack() 33 | Label(fileFrame, text=">> 初始化:项目多语言仓库初始化", justify=CENTER).grid(row=1, column=1) 34 | Label(fileFrame, text="1. 选择 Android 多语言资源根目录:", justify=LEFT).grid(row=2, column=1) 35 | Entry(fileFrame, textvariable=self.android_resources_dir).grid(row=2, column=2) 36 | Button(fileFrame, text="选择", command=self.select_android_directory).grid(row=2, column=3) 37 | Label(fileFrame, text="2. 选择 iOS 多语言资源根目录:", justify=LEFT).grid(row=3, column=1) 38 | Entry(fileFrame, textvariable=self.ios_resources_dir).grid(row=3, column=2) 39 | Button(fileFrame, text="选择", command=self.select_ios_directory).grid(row=3, column=3) 40 | # 选择要支持的语言 41 | languageFrame = Frame(root) 42 | languageFrame.pack() 43 | colCount = -1 44 | for k,v in appConfig.languages.items(): 45 | colCount = colCount + 1 46 | self.support_languages[k] = BooleanVar() 47 | Checkbutton(languageFrame, text=k, variable=self.support_languages[k]).grid(row=2, column=colCount) 48 | # 初始化按钮 49 | startFrame = Frame(root) 50 | startFrame.pack() 51 | Button(startFrame, text="初始化", command=self.initialize_repo).grid(row=1, column=1) 52 | 53 | # 选择 Android 多语言资源根目录 54 | def select_android_directory(self): 55 | self.android_resources_dir.set(askdirectory()) 56 | logging.debug(self.android_resources_dir) 57 | 58 | # 选择 iOS 多语言资源根目录 59 | def select_ios_directory(self): 60 | self.ios_resources_dir.set(askdirectory()) 61 | logging.debug(self.ios_resources_dir) 62 | 63 | # 初始化仓库 64 | def initialize_repo(self): 65 | # 初始化应用配置 66 | support_languages = [] 67 | for k,v in self.support_languages.items(): 68 | if v.get(): 69 | for language in appConfig.languages[k]: 70 | support_languages.append(language) 71 | # 初始化项目仓库 72 | init = Initializer() 73 | init.initialize(self.android_resources_dir.get(), self.ios_resources_dir.get(), support_languages) 74 | # 初始化完毕 75 | result = askokcancel(title = '初始化完成', message='已完成项目仓库初始化,请重启程序') 76 | if result: 77 | self.root.quit() 78 | 79 | # 主程序页面 80 | class MainDialog(Frame): 81 | # 初始化 82 | def __init__(self, root): 83 | self.importer = Importer(appConfig) 84 | self.generator = Generator(appConfig) 85 | self.translate_progress = StringVar() 86 | self.translate_started = False 87 | Frame.__init__(self, root) 88 | frame = Frame(root) 89 | frame.pack() 90 | # 更新词条 91 | # Label(frame, text="", justify=LEFT).grid(row=1, column=1) 92 | Label(frame, text="1.更新词条", justify=LEFT).grid(row=2, column=1) 93 | Button(frame, text="更新 Android 多语言词条", command=self.update_android_resource).grid(row=2, column=2) 94 | Button(frame, text="更新 iOS 多语言词条", command=self.update_ios_resource).grid(row=2, column=3) 95 | # 自助翻译 96 | # Label(frame, text="", justify=LEFT).grid(row=3, column=1) 97 | Label(frame, text="2.自助翻译", justify=LEFT).grid(row=4, column=1) 98 | Button(frame, text="自动翻译", command=self.auto_translate).grid(row=4, column=2) 99 | Label(frame, textvariable=self.translate_progress).grid(row=4, column=3) 100 | # 导入翻译资源 导出翻译资源 101 | # Label(frame, text="", justify=LEFT).grid(row=5, column=1) 102 | Label(frame, text="3.人工翻译", justify=LEFT).grid(row=6, column=1) 103 | Button(frame, text="导出翻译资源(Excel)", command=self.generate_translate_resources).grid(row=6, column=2) 104 | Button(frame, text="导入翻译资源(Excel)", command=self.import_translated_excel).grid(row=6, column=3) 105 | # 生成多语言资源 106 | # Label(frame, text="", justify=LEFT).grid(row=7, column=1) 107 | Label(frame, text="4.生成资源", justify=LEFT).grid(row=8, column=1) 108 | Button(frame, text="生成 Android 多语言资源", command=self.generate_android_resources).grid(row=8, column=2) 109 | Button(frame, text="生成 iOS 多语言资源", command=self.generate_ios_resources).grid(row=8, column=3) 110 | # 校验多语言资源 111 | # Label(frame, text="", justify=LEFT).grid(row=9, column=1) 112 | Label(frame, text="5.校验资源", justify=LEFT).grid(row=10, column=1) 113 | Button(frame, text="将 Android 多语言资源修改结果同步到仓库", command=self.import_modified_android_resource).grid(row=10, column=2) 114 | Button(frame, text="将 iOS 多语言资源修改结果同步到仓库", command=self.import_modified_ios_resource).grid(row=10, column=3) 115 | 116 | # 将 Android 多语言资源修改结果同步到仓库 117 | def import_modified_android_resource(self): 118 | # 如果没有设置过多语言根目录就设置下 119 | if len(appConfig.android_resources_root_directory) == 0: 120 | showinfo(title='提示', message='您在初始化项目的时候并没有为 Android 指定多语言根目录,无法完成更新。您可以尝试备份并删除 repo.json 文件重新初始化项目仓库。') 121 | return 122 | # 开始更新 123 | self.importer.import_modified_android_resource() 124 | showinfo(title='更新完成', message='已更新到多语言仓库!') 125 | 126 | # 将 iOS 多语言资源修改结果同步到仓库 127 | def import_modified_ios_resource(self): 128 | # 如果没有设置过多语言根目录就设置下 129 | if len(appConfig.ios_resources_root_directory) == 0: 130 | showinfo(title='提示', message='您在初始化项目的时候并没有为 iOS 指定多语言根目录,无法完成更新。您可以尝试备份并删除 repo.json 文件重新初始化项目仓库。') 131 | return 132 | # 开始更新 133 | self.importer.import_modified_ios_resource() 134 | showinfo(title='更新完成', message='已更新到多语言仓库!') 135 | 136 | # 生成 Android 多语言资源 137 | def generate_android_resources(self): 138 | # 判断没有翻译的词条数量 139 | ret = repository.get_repo_state() 140 | missed_cuount = ret["missed_count"] 141 | if missed_cuount != 0: 142 | result = askokcancel(title="警告", message="存在 %d 个词条没有完全翻译!仍然生成?" % missed_cuount) 143 | if result: 144 | self.__generate_android_resources_finaly() 145 | else: 146 | self.__generate_android_resources_finaly() 147 | 148 | # 生成 iOS 多语言资源 149 | def generate_ios_resources(self): 150 | # 判断没有翻译的词条数量 151 | ret = repository.get_repo_state() 152 | missed_cuount = ret["missed_count"] 153 | if missed_cuount != 0: 154 | result = askokcancel(title="警告", message="存在 %d 个词条没有完全翻译!仍然生成?" % missed_cuount) 155 | if result: 156 | self.__generate_ios_resources_finaly() 157 | else: 158 | self.__generate_ios_resources_finaly() 159 | 160 | # 生成用来翻译的 Excel 表格 161 | def generate_translate_resources(self): 162 | # 导出到的文件夹 163 | appConfig.translate_excel_output_directory = askdirectory() 164 | appConfig.write_to_json() 165 | # 导出 Excel 文件 166 | self.generator.gen_translate_excel(appConfig.translate_excel_output_directory) 167 | showinfo(title='导出完成', message='已导出翻译 Excel 到 %s !' % appConfig.translate_excel_output_directory) 168 | 169 | # 导入翻译资源 170 | def import_translated_excel(self): 171 | f = askopenfilename(title='选择 Excel 文件', filetypes=[('Excel', '*.xlsx'), ('All Files', '*')]) 172 | self.importer.import_translated_excel(f) 173 | showinfo(title='更新完成', message='已更新到多语言仓库!') 174 | 175 | # 更新 Android 多语言 176 | def update_android_resource(self): 177 | # 如果没有设置过多语言根目录就设置下 178 | if len(appConfig.android_resources_root_directory) == 0: 179 | showinfo(title='提示', message='您在初始化项目的时候并没有为 Android 指定多语言根目录,无法完成更新。您可以尝试备份并删除 repo.json 文件重新初始化项目仓库。') 180 | return 181 | # 开始更新 182 | self.importer.update_android_resource() 183 | showinfo(title='更新完成', message='已更新到多语言仓库!') 184 | 185 | # 更新 iOS 多语言 186 | def update_ios_resource(self): 187 | # 如果没有设置过多语言根目录就设置下 188 | if len(appConfig.ios_resources_root_directory) == 0: 189 | showinfo(title='提示', message='您在初始化项目的时候并没有为 iOS 指定多语言根目录,无法完成更新。您可以尝试备份并删除 repo.json 文件重新初始化项目仓库。') 190 | return 191 | # 开始更新 192 | self.importer.update_ios_resource() 193 | showinfo(title='更新完成', message='已更新到多语言仓库!') 194 | 195 | # 自动进行多语言翻译 196 | def auto_translate(self): 197 | bd = BaiduTranslator() 198 | if not bd.is_configed(): 199 | showinfo(title='百度 API 没有配置', message='请在 config/baidu.json 文件中填写您在平台申请的 appid 和 appsecret 之后再尝试!') 200 | return 201 | ret = repository.get_repo_state() 202 | missed_cuount = ret["missed_count"] 203 | if self.translate_started: 204 | showinfo(title='翻译已启动', message='翻译已经启动,程序正在翻译中……') 205 | return 206 | if missed_cuount == 0: 207 | showinfo(title='已全部翻译完成', message='所有词条已经翻译完毕,无需进行自动翻译') 208 | else: 209 | thread = threading.Thread(target=self.__start_translate) 210 | thread.start() 211 | self.translate_started = True 212 | 213 | # 正在开始执行翻译 214 | def __start_translate(self): 215 | translator = Translator() 216 | translator.start_translate(self.on_translation_progress_changed, self.on_translation_finished) 217 | 218 | # 通知翻译进度变化 219 | def on_translation_progress_changed(self, progress): 220 | logging.debug("On translation progress changed " + str(progress)) 221 | self.translate_progress.set("当前进度: %d%%" % progress) 222 | 223 | # 增加一个完成的回调 224 | def on_translation_finished(self): 225 | self.translate_started = False 226 | showinfo(title='翻译完成', message='已完成翻译任务') 227 | 228 | # 生成 iOS 资源目录 229 | def __generate_ios_resources_finaly(self): 230 | # 如果没设置 iOS 资源目录,则需要选择下 231 | if len(appConfig.ios_resources_root_directory) == 0: 232 | showinfo(title='提示', message='请先选择 iOS 多语言资源根目录') 233 | appConfig.ios_resources_root_directory = askdirectory() 234 | appConfig.write_to_json() 235 | # 生成 236 | self.generator.gen_ios_resources() 237 | showinfo(title='导出完成', message='已导出 iOS 多语言文件到 ' + appConfig.ios_resources_root_directory) 238 | 239 | # 生成 Android 资源目录 240 | def __generate_android_resources_finaly(self): 241 | # 如果没设置 Android 资源目录,则需要选择下 242 | if len(appConfig.android_resources_root_directory) == 0: 243 | showinfo(title='提示', message='请先选择 Android 多语言资源根目录') 244 | appConfig.android_resources_root_directory = askdirectory() 245 | appConfig.write_to_json() 246 | # 生成 247 | self.generator.gen_android_resources() 248 | showinfo(title='导出完成', message='已导出 Android 多语言文件到 ' + appConfig.android_resources_root_directory) 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /android/am.md: -------------------------------------------------------------------------------- 1 | Activity manager (activity) commands: 2 | help 3 | Print this help text. 4 | start-activity [-D] [-N] [-W] [-P ] [--start-profiler ] 5 | [--sampling INTERVAL] [--streaming] [-R COUNT] [-S] 6 | [--track-allocation] [--user | current] 7 | Start an Activity. Options are: 8 | -D: enable debugging 9 | -N: enable native debugging 10 | -W: wait for launch to complete 11 | --start-profiler : start profiler and send results to 12 | --sampling INTERVAL: use sample profiling with INTERVAL microseconds 13 | between samples (use with --start-profiler) 14 | --streaming: stream the profiling output to the specified file 15 | (use with --start-profiler) 16 | -P : like above, but profiling stops when app goes idle 17 | --attach-agent : attach the given agent before binding 18 | --attach-agent-bind : attach the given agent during binding 19 | -R: repeat the activity launch times. Prior to each repeat, 20 | the top activity will be finished. 21 | -S: force stop the target app before starting the activity 22 | --track-allocation: enable tracking of object allocations 23 | --user | current: Specify which user to run as; if not 24 | specified then run as the current user. 25 | --windowingMode : The windowing mode to launch the activity into. 26 | --activityType : The activity type to launch the activity as. 27 | --display : The display to launch the activity into. 28 | start-service [--user | current] 29 | Start a Service. Options are: 30 | --user | current: Specify which user to run as; if not 31 | specified then run as the current user. 32 | start-foreground-service [--user | current] 33 | Start a foreground Service. Options are: 34 | --user | current: Specify which user to run as; if not 35 | specified then run as the current user. 36 | stop-service [--user | current] 37 | Stop a Service. Options are: 38 | --user | current: Specify which user to run as; if not 39 | specified then run as the current user. 40 | broadcast [--user | all | current] 41 | Send a broadcast Intent. Options are: 42 | --user | all | current: Specify which user to send to; if not 43 | specified then send to all users. 44 | --receiver-permission : Require receiver to hold permission. 45 | --allow-background-activity-starts: The receiver may start activities 46 | even if in the background. 47 | instrument [-r] [-e ] [-p ] [-w] 48 | [--user | current] 49 | [--no-hidden-api-checks [--no-test-api-access]] 50 | [--no-isolated-storage] 51 | [--no-window-animation] [--abi ] 52 | Start an Instrumentation. Typically this target is in the 53 | form / or only if there 54 | is only one instrumentation. Options are: 55 | -r: print raw results (otherwise decode REPORT_KEY_STREAMRESULT). Use with 56 | [-e perf true] to generate raw output for performance measurements. 57 | -e : set argument to . For test runners a 58 | common form is [-e [,...]]. 59 | -p : write profiling data to 60 | -m: Write output as protobuf to stdout (machine readable) 61 | -f : Write output as protobuf to a file (machine 62 | readable). If path is not specified, default directory and file name will 63 | be used: /sdcard/instrument-logs/log-yyyyMMdd-hhmmss-SSS.instrumentation_data_proto 64 | -w: wait for instrumentation to finish before returning. Required for 65 | test runners. 66 | --user | current: Specify user instrumentation runs in; 67 | current user if not specified. 68 | --no-hidden-api-checks: disable restrictions on use of hidden API. 69 | --no-test-api-access: do not allow access to test APIs, if hidden 70 | API checks are enabled. 71 | --no-isolated-storage: don't use isolated storage sandbox and 72 | mount full external storage 73 | --no-window-animation: turn off window animations while running. 74 | --abi : Launch the instrumented process with the selected ABI. 75 | This assumes that the process supports the selected ABI. 76 | trace-ipc [start|stop] [--dump-file ] 77 | Trace IPC transactions. 78 | start: start tracing IPC transactions. 79 | stop: stop tracing IPC transactions and dump the results to file. 80 | --dump-file : Specify the file the trace should be dumped to. 81 | profile start [--user current] 82 | [--sampling INTERVAL | --streaming] 83 | Start profiler on a process. The given argument 84 | may be either a process name or pid. Options are: 85 | --user | current: When supplying a process name, 86 | specify user of process to profile; uses current user if not 87 | specified. 88 | --sampling INTERVAL: use sample profiling with INTERVAL microseconds 89 | between samples. 90 | --streaming: stream the profiling output to the specified file. 91 | profile stop [--user current] 92 | Stop profiler on a process. The given argument 93 | may be either a process name or pid. Options are: 94 | --user | current: When supplying a process name, 95 | specify user of process to profile; uses current user if not 96 | specified. 97 | dumpheap [--user current] [-n] [-g] 98 | Dump the heap of a process. The given argument may 99 | be either a process name or pid. Options are: 100 | -n: dump native heap instead of managed heap 101 | -g: force GC before dumping the heap 102 | --user | current: When supplying a process name, 103 | specify user of process to dump; uses current user if not specified. 104 | set-debug-app [-w] [--persistent] 105 | Set application to debug. Options are: 106 | -w: wait for debugger when application starts 107 | --persistent: retain this value 108 | clear-debug-app 109 | Clear the previously set-debug-app. 110 | set-watch-heap 111 | Start monitoring pss size of , if it is at or 112 | above then a heap dump is collected for the user to report. 113 | clear-watch-heap 114 | Clear the previously set-watch-heap. 115 | clear-exit-info [--user | all | current] [package] 116 | Clear the process exit-info for given package 117 | bug-report [--progress | --telephony] 118 | Request bug report generation; will launch a notification 119 | when done to select where it should be delivered. Options are: 120 | --progress: will launch a notification right away to show its progress. 121 | --telephony: will dump only telephony sections. 122 | force-stop [--user | all | current] 123 | Completely stop the given application package. 124 | crash [--user ] 125 | Induce a VM crash in the specified package or process 126 | kill [--user | all | current] 127 | Kill all background processes associated with the given application. 128 | kill-all 129 | Kill all processes that are safe to kill (cached, etc). 130 | make-uid-idle [--user | all | current] 131 | If the given application's uid is in the background and waiting to 132 | become idle (not allowing background services), do that now. 133 | monitor [--gdb ] 134 | Start monitoring for crashes or ANRs. 135 | --gdb: start gdbserv on the given port at crash/ANR 136 | watch-uids [--oom ] 137 | Start watching for and reporting uid state changes. 138 | --oom: specify a uid for which to report detailed change messages. 139 | hang [--allow-restart] 140 | Hang the system. 141 | --allow-restart: allow watchdog to perform normal system restart 142 | restart 143 | Restart the user-space system. 144 | idle-maintenance 145 | Perform idle maintenance now. 146 | screen-compat [on|off] 147 | Control screen compatibility mode of . 148 | package-importance 149 | Print current importance of . 150 | to-uri [INTENT] 151 | Print the given Intent specification as a URI. 152 | to-intent-uri [INTENT] 153 | Print the given Intent specification as an intent: URI. 154 | to-app-uri [INTENT] 155 | Print the given Intent specification as an android-app: URI. 156 | switch-user 157 | Switch to put USER_ID in the foreground, starting 158 | execution of that user if it is currently stopped. 159 | get-current-user 160 | Returns id of the current foreground user. 161 | start-user [-w] 162 | Start USER_ID in background if it is currently stopped; 163 | use switch-user if you want to start the user in foreground. 164 | -w: wait for start-user to complete and the user to be unlocked. 165 | unlock-user [TOKEN_HEX] 166 | Attempt to unlock the given user using the given authorization token. 167 | stop-user [-w] [-f] 168 | Stop execution of USER_ID, not allowing it to run any 169 | code until a later explicit start or switch to it. 170 | -w: wait for stop-user to complete. 171 | -f: force stop even if there are related users that cannot be stopped. 172 | is-user-stopped 173 | Returns whether has been stopped or not. 174 | get-started-user-state 175 | Gets the current state of the given started user. 176 | track-associations 177 | Enable association tracking. 178 | untrack-associations 179 | Disable and clear association tracking. 180 | get-uid-state 181 | Gets the process state of an app given its . 182 | attach-agent 183 | Attach an agent to the specified , which may be either a process name or a PID. 184 | get-config [--days N] [--device] [--proto] [--display ] 185 | Retrieve the configuration and any recent configurations of the device. 186 | --days: also return last N days of configurations that have been seen. 187 | --device: also output global device configuration info. 188 | --proto: return result as a proto; does not include --days info. 189 | --display: Specify for which display to run the command; if not 190 | specified then run for the default display. 191 | supports-multiwindow 192 | Returns true if the device supports multiwindow. 193 | supports-split-screen-multi-window 194 | Returns true if the device supports split screen multiwindow. 195 | suppress-resize-config-changes 196 | Suppresses configuration changes due to user resizing an activity/task. 197 | set-inactive [--user ] true|false 198 | Sets the inactive state of an app. 199 | get-inactive [--user ] 200 | Returns the inactive state of an app. 201 | set-standby-bucket [--user ] active|working_set|frequent|rare 202 | Puts an app in the standby bucket. 203 | get-standby-bucket [--user ] 204 | Returns the standby bucket of an app. 205 | send-trim-memory [--user ] 206 | [HIDDEN|RUNNING_MODERATE|BACKGROUND|RUNNING_LOW|MODERATE|RUNNING_CRITICAL|COMPLETE] 207 | Send a memory trim event to a . May also supply a raw trim int level. 208 | display [COMMAND] [...]: sub-commands for operating on displays. 209 | move-stack 210 | Move from its current display to . 211 | stack [COMMAND] [...]: sub-commands for operating on activity stacks. 212 | move-task [true|false] 213 | Move from its current stack to the top (true) or 214 | bottom (false) of . 215 | resize-docked-stack [] 216 | Change docked stack to 217 | and supplying temporary different task bounds indicated by 218 | 219 | move-top-activity-to-pinned-stack: 220 | Moves the top activity from 221 | to the pinned stack using for the 222 | bounds of the pinned stack. 223 | positiontask 224 | Place in at 225 | list 226 | List all of the activity stacks and their sizes. 227 | info 228 | Display the information about activity stack in and . 229 | remove 230 | Remove stack . 231 | task [COMMAND] [...]: sub-commands for operating on activity tasks. 232 | lock 233 | Bring to the front and don't allow other tasks to run. 234 | lock stop 235 | End the current task lock. 236 | resizeable [0|1|2|3] 237 | Change resizeable mode of to one of the following: 238 | 0: unresizeable 239 | 1: crop_windows 240 | 2: resizeable 241 | 3: resizeable_and_pipable 242 | resize 243 | Makes sure is in a stack with the specified bounds. 244 | Forces the task to be resizeable and creates a stack if no existing stack 245 | has the specified bounds. 246 | update-appinfo [...] 247 | Update the ApplicationInfo objects of the listed packages for 248 | without restarting any processes. 249 | write 250 | Write all pending state to storage. 251 | compat [COMMAND] [...]: sub-commands for toggling app-compat changes. 252 | enable|disable|reset 253 | Toggles a change either by id or by name for . 254 | It kills (to allow the toggle to take effect). 255 | enable-all|disable-all . 257 | reset-all 258 | Removes all existing overrides for all changes for 259 | (back to default behaviour). 260 | It kills (to allow the toggle to take effect). 261 | 262 | specifications include these flags and arguments: 263 | [-a ] [-d ] [-t ] [-i ] 264 | [-c [-c ] ...] 265 | [-n ] 266 | [-e|--es ...] 267 | [--esn ...] 268 | [--ez ...] 269 | [--ei ...] 270 | [--el ...] 271 | [--ef ...] 272 | [--eu ...] 273 | [--ecn ] 274 | [--eia [, [,) 278 | [--ela [, [,) 282 | [--efa [, [,) 286 | [--esa [, [,; to embed a comma into a string, 291 | escape it using "\,") 292 | [-f ] 293 | [--grant-read-uri-permission] [--grant-write-uri-permission] 294 | [--grant-persistable-uri-permission] [--grant-prefix-uri-permission] 295 | [--debug-log-resolution] [--exclude-stopped-packages] 296 | [--include-stopped-packages] 297 | [--activity-brought-to-front] [--activity-clear-top] 298 | [--activity-clear-when-task-reset] [--activity-exclude-from-recents] 299 | [--activity-launched-from-history] [--activity-multiple-task] 300 | [--activity-no-animation] [--activity-no-history] 301 | [--activity-no-user-action] [--activity-previous-is-top] 302 | [--activity-reorder-to-front] [--activity-reset-task-if-needed] 303 | [--activity-single-top] [--activity-clear-task] 304 | [--activity-task-on-home] [--activity-match-external] 305 | [--receiver-registered-only] [--receiver-replace-pending] 306 | [--receiver-foreground] [--receiver-no-abort] 307 | [--receiver-include-background] 308 | [--selector] 309 | [ | | ] 310 | -------------------------------------------------------------------------------- /android/pm.md: -------------------------------------------------------------------------------- 1 | Package manager (package) commands: 2 | help 3 | Print this help text. 4 | 5 | path [--user USER_ID] PACKAGE 6 | Print the path to the .apk of the given PACKAGE. 7 | 8 | dump PACKAGE 9 | Print various system state associated with the given PACKAGE. 10 | 11 | has-feature FEATURE_NAME [version] 12 | Prints true and returns exit status 0 when system has a FEATURE_NAME, 13 | otherwise prints false and returns exit status 1 14 | 15 | list features 16 | Prints all features of the system. 17 | 18 | list instrumentation [-f] [TARGET-PACKAGE] 19 | Prints all test packages; optionally only those targeting TARGET-PACKAGE 20 | Options: 21 | -f: dump the name of the .apk file containing the test package 22 | 23 | list libraries 24 | Prints all system libraries. 25 | 26 | list packages [-f] [-d] [-e] [-s] [-3] [-i] [-l] [-u] [-U] 27 | [--show-versioncode] [--apex-only] [--uid UID] [--user USER_ID] [FILTER] 28 | Prints all packages; optionally only those whose name contains 29 | the text in FILTER. Options are: 30 | -f: see their associated file 31 | -a: all known packages (but excluding APEXes) 32 | -d: filter to only show disabled packages 33 | -e: filter to only show enabled packages 34 | -s: filter to only show system packages 35 | -3: filter to only show third party packages 36 | -i: see the installer for the packages 37 | -l: ignored (used for compatibility with older releases) 38 | -U: also show the package UID 39 | -u: also include uninstalled packages 40 | --show-versioncode: also show the version code 41 | --apex-only: only show APEX packages 42 | --uid UID: filter to only show packages with the given UID 43 | --user USER_ID: only list packages belonging to the given user 44 | 45 | list permission-groups 46 | Prints all known permission groups. 47 | 48 | list permissions [-g] [-f] [-d] [-u] [GROUP] 49 | Prints all known permissions; optionally only those in GROUP. Options are: 50 | -g: organize by group 51 | -f: print all information 52 | -s: short summary 53 | -d: only list dangerous permissions 54 | -u: list only the permissions users will see 55 | 56 | list staged-sessions [--only-ready] [--only-sessionid] [--only-parent] 57 | Prints all staged sessions. 58 | --only-ready: show only staged sessions that are ready 59 | --only-sessionid: show only sessionId of each session 60 | --only-parent: hide all children sessions 61 | 62 | list users 63 | Prints all users. 64 | 65 | resolve-activity [--brief] [--components] [--query-flags FLAGS] 66 | [--user USER_ID] INTENT 67 | Prints the activity that resolves to the given INTENT. 68 | 69 | query-activities [--brief] [--components] [--query-flags FLAGS] 70 | [--user USER_ID] INTENT 71 | Prints all activities that can handle the given INTENT. 72 | 73 | query-services [--brief] [--components] [--query-flags FLAGS] 74 | [--user USER_ID] INTENT 75 | Prints all services that can handle the given INTENT. 76 | 77 | query-receivers [--brief] [--components] [--query-flags FLAGS] 78 | [--user USER_ID] INTENT 79 | Prints all broadcast receivers that can handle the given INTENT. 80 | 81 | install [-rtfdgw] [-i PACKAGE] [--user USER_ID|all|current] 82 | [-p INHERIT_PACKAGE] [--install-location 0/1/2] 83 | [--install-reason 0/1/2/3/4] [--originating-uri URI] 84 | [--referrer URI] [--abi ABI_NAME] [--force-sdk] 85 | [--preload] [--instant] [--full] [--dont-kill] 86 | [--enable-rollback] 87 | [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES] 88 | [--apex] [--wait TIMEOUT] 89 | [PATH [SPLIT...]|-] 90 | Install an application. Must provide the apk data to install, either as 91 | file path(s) or '-' to read from stdin. Options are: 92 | -R: disallow replacement of existing application 93 | -t: allow test packages 94 | -i: specify package name of installer owning the app 95 | -f: install application on internal flash 96 | -d: allow version code downgrade (debuggable packages only) 97 | -p: partial application install (new split on top of existing pkg) 98 | -g: grant all runtime permissions 99 | -S: size in bytes of package, required for stdin 100 | --user: install under the given user. 101 | --dont-kill: installing a new feature split, don't kill running app 102 | --restrict-permissions: don't whitelist restricted permissions at install 103 | --originating-uri: set URI where app was downloaded from 104 | --referrer: set URI that instigated the install of the app 105 | --pkg: specify expected package name of app being installed 106 | --abi: override the default ABI of the platform 107 | --instant: cause the app to be installed as an ephemeral install app 108 | --full: cause the app to be installed as a non-ephemeral full app 109 | --install-location: force the install location: 110 | 0=auto, 1=internal only, 2=prefer external 111 | --install-reason: indicates why the app is being installed: 112 | 0=unknown, 1=admin policy, 2=device restore, 113 | 3=device setup, 4=user request 114 | --force-uuid: force install on to disk volume with given UUID 115 | --apex: install an .apex file, not an .apk 116 | --wait: when performing staged install, wait TIMEOUT milliseconds 117 | for pre-reboot verification to complete. If TIMEOUT is not 118 | specified it will wait for 60000 milliseconds. 119 | 120 | install-existing [--user USER_ID|all|current] 121 | [--instant] [--full] [--wait] [--restrict-permissions] PACKAGE 122 | Installs an existing application for a new user. Options are: 123 | --user: install for the given user. 124 | --instant: install as an instant app 125 | --full: install as a full app 126 | --wait: wait until the package is installed 127 | --restrict-permissions: don't whitelist restricted permissions 128 | 129 | install-create [-lrtsfdg] [-i PACKAGE] [--user USER_ID|all|current] 130 | [-p INHERIT_PACKAGE] [--install-location 0/1/2] 131 | [--install-reason 0/1/2/3/4] [--originating-uri URI] 132 | [--referrer URI] [--abi ABI_NAME] [--force-sdk] 133 | [--preload] [--instant] [--full] [--dont-kill] 134 | [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES] 135 | [--multi-package] [--staged] 136 | Like "install", but starts an install session. Use "install-write" 137 | to push data into the session, and "install-commit" to finish. 138 | 139 | install-write [-S BYTES] SESSION_ID SPLIT_NAME [PATH|-] 140 | Write an apk into the given install session. If the path is '-', data 141 | will be read from stdin. Options are: 142 | -S: size in bytes of package, required for stdin 143 | 144 | install-remove SESSION_ID SPLIT... 145 | Mark SPLIT(s) as removed in the given install session. 146 | 147 | install-add-session MULTI_PACKAGE_SESSION_ID CHILD_SESSION_IDs 148 | Add one or more session IDs to a multi-package session. 149 | 150 | install-commit SESSION_ID 151 | Commit the given active install session, installing the app. 152 | 153 | install-abandon SESSION_ID 154 | Delete the given active install session. 155 | 156 | set-install-location LOCATION 157 | Changes the default install location. NOTE this is only intended for debugging; 158 | using this can cause applications to break and other undersireable behavior. 159 | LOCATION is one of: 160 | 0 [auto]: Let system decide the best location 161 | 1 [internal]: Install on internal device storage 162 | 2 [external]: Install on external media 163 | 164 | get-install-location 165 | Returns the current install location: 0, 1 or 2 as per set-install-location. 166 | 167 | move-package PACKAGE [internal|UUID] 168 | 169 | move-primary-storage [internal|UUID] 170 | 171 | uninstall [-k] [--user USER_ID] [--versionCode VERSION_CODE] 172 | PACKAGE [SPLIT...] 173 | Remove the given package name from the system. May remove an entire app 174 | if no SPLIT names specified, otherwise will remove only the splits of the 175 | given app. Options are: 176 | -k: keep the data and cache directories around after package removal. 177 | --user: remove the app from the given user. 178 | --versionCode: only uninstall if the app has the given version code. 179 | 180 | clear [--user USER_ID] PACKAGE 181 | Deletes all data associated with a package. 182 | 183 | enable [--user USER_ID] PACKAGE_OR_COMPONENT 184 | disable [--user USER_ID] PACKAGE_OR_COMPONENT 185 | disable-user [--user USER_ID] PACKAGE_OR_COMPONENT 186 | disable-until-used [--user USER_ID] PACKAGE_OR_COMPONENT 187 | default-state [--user USER_ID] PACKAGE_OR_COMPONENT 188 | These commands change the enabled state of a given package or 189 | component (written as "package/class"). 190 | 191 | hide [--user USER_ID] PACKAGE_OR_COMPONENT 192 | unhide [--user USER_ID] PACKAGE_OR_COMPONENT 193 | 194 | suspend [--user USER_ID] TARGET-PACKAGE 195 | Suspends the specified package (as user). 196 | 197 | unsuspend [--user USER_ID] TARGET-PACKAGE 198 | Unsuspends the specified package (as user). 199 | 200 | grant [--user USER_ID] PACKAGE PERMISSION 201 | revoke [--user USER_ID] PACKAGE PERMISSION 202 | These commands either grant or revoke permissions to apps. The permissions 203 | must be declared as used in the app's manifest, be runtime permissions 204 | (protection level dangerous), and the app targeting SDK greater than Lollipop MR1. 205 | 206 | reset-permissions 207 | Revert all runtime permissions to their default state. 208 | 209 | set-permission-enforced PERMISSION [true|false] 210 | 211 | get-privapp-permissions TARGET-PACKAGE 212 | Prints all privileged permissions for a package. 213 | 214 | get-privapp-deny-permissions TARGET-PACKAGE 215 | Prints all privileged permissions that are denied for a package. 216 | 217 | get-oem-permissions TARGET-PACKAGE 218 | Prints all OEM permissions for a package. 219 | 220 | set-app-link [--user USER_ID] PACKAGE {always|ask|never|undefined} 221 | get-app-link [--user USER_ID] PACKAGE 222 | 223 | trim-caches DESIRED_FREE_SPACE [internal|UUID] 224 | Trim cache files to reach the given free space. 225 | 226 | list users 227 | Lists the current users. 228 | 229 | create-user [--profileOf USER_ID] [--managed] [--restricted] [--ephemeral] 230 | [--guest] [--pre-create-only] [--user-type USER_TYPE] USER_NAME 231 | Create a new user with the given USER_NAME, printing the new user identifier 232 | of the user. 233 | USER_TYPE is the name of a user type, e.g. android.os.usertype.profile.MANAGED. 234 | If not specified, the default user type is android.os.usertype.full.SECONDARY. 235 | --managed is shorthand for '--user-type android.os.usertype.profile.MANAGED'. 236 | --restricted is shorthand for '--user-type android.os.usertype.full.RESTRICTED'. 237 | --guest is shorthand for '--user-type android.os.usertype.full.GUEST'. 238 | 239 | remove-user USER_ID 240 | Remove the user with the given USER_IDENTIFIER, deleting all data 241 | associated with that user 242 | 243 | set-user-restriction [--user USER_ID] RESTRICTION VALUE 244 | 245 | get-max-users 246 | 247 | get-max-running-users 248 | 249 | compile [-m MODE | -r REASON] [-f] [-c] [--split SPLIT_NAME] 250 | [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE) 251 | Trigger compilation of TARGET-PACKAGE or all packages if "-a". Options are: 252 | -a: compile all packages 253 | -c: clear profile data before compiling 254 | -f: force compilation even if not needed 255 | -m: select compilation mode 256 | MODE is one of the dex2oat compiler filters: 257 | assume-verified 258 | extract 259 | verify 260 | quicken 261 | space-profile 262 | space 263 | speed-profile 264 | speed 265 | everything 266 | -r: select compilation reason 267 | REASON is one of: 268 | first-boot 269 | boot 270 | install 271 | bg-dexopt 272 | ab-ota 273 | inactive 274 | shared 275 | --reset: restore package to its post-install state 276 | --check-prof (true | false): look at profiles when doing dexopt? 277 | --secondary-dex: compile app secondary dex files 278 | --split SPLIT: compile only the given split name 279 | --compile-layouts: compile layout resources for faster inflation 280 | 281 | force-dex-opt PACKAGE 282 | Force immediate execution of dex opt for the given PACKAGE. 283 | 284 | bg-dexopt-job 285 | Execute the background optimizations immediately. 286 | Note that the command only runs the background optimizer logic. It may 287 | overlap with the actual job but the job scheduler will not be able to 288 | cancel it. It will also run even if the device is not in the idle 289 | maintenance mode. 290 | 291 | reconcile-secondary-dex-files TARGET-PACKAGE 292 | Reconciles the package secondary dex files with the generated oat files. 293 | 294 | dump-profiles TARGET-PACKAGE 295 | Dumps method/class profile files to 296 | /data/misc/profman/TARGET-PACKAGE.txt 297 | 298 | snapshot-profile TARGET-PACKAGE [--code-path path] 299 | Take a snapshot of the package profiles to 300 | /data/misc/profman/TARGET-PACKAGE[-code-path].prof 301 | If TARGET-PACKAGE=android it will take a snapshot of the boot image 302 | 303 | set-home-activity [--user USER_ID] TARGET-COMPONENT 304 | Set the default home activity (aka launcher). 305 | TARGET-COMPONENT can be a package name (com.package.my) or a full 306 | component (com.package.my/component.name). However, only the package name 307 | matters: the actual component used will be determined automatically from 308 | the package. 309 | 310 | set-installer PACKAGE INSTALLER 311 | Set installer package name 312 | 313 | get-instantapp-resolver 314 | Return the name of the component that is the current instant app installer. 315 | 316 | set-harmful-app-warning [--user ] [] 317 | Mark the app as harmful with the given warning message. 318 | 319 | get-harmful-app-warning [--user ] 320 | Return the harmful app warning message for the given app, if present 321 | 322 | uninstall-system-updates [] 323 | Removes updates to the given system application and falls back to its 324 | /system version. Does nothing if the given package is not a system app. 325 | If no package is specified, removes updates to all system applications. 326 | 327 | get-moduleinfo [--all | --installed] [module-name] 328 | Displays module info. If module-name is specified only that info is shown 329 | By default, without any argument only installed modules are shown. 330 | --all: show all module info 331 | --installed: show only installed modules 332 | 333 | log-visibility [--enable|--disable] 334 | Turns on debug logging when visibility is blocked for the given package. 335 | --enable: turn on debug logging (default) 336 | --disable: turn off debug logging 337 | 338 | specifications include these flags and arguments: 339 | [-a ] [-d ] [-t ] [-i ] 340 | [-c [-c ] ...] 341 | [-n ] 342 | [-e|--es ...] 343 | [--esn ...] 344 | [--ez ...] 345 | [--ei ...] 346 | [--el ...] 347 | [--ef ...] 348 | [--eu ...] 349 | [--ecn ] 350 | [--eia [, [,) 354 | [--ela [, [,) 358 | [--efa [, [,) 362 | [--esa [, [,; to embed a comma into a string, 367 | escape it using "\,") 368 | [-f ] 369 | [--grant-read-uri-permission] [--grant-write-uri-permission] 370 | [--grant-persistable-uri-permission] [--grant-prefix-uri-permission] 371 | [--debug-log-resolution] [--exclude-stopped-packages] 372 | [--include-stopped-packages] 373 | [--activity-brought-to-front] [--activity-clear-top] 374 | [--activity-clear-when-task-reset] [--activity-exclude-from-recents] 375 | [--activity-launched-from-history] [--activity-multiple-task] 376 | [--activity-no-animation] [--activity-no-history] 377 | [--activity-no-user-action] [--activity-previous-is-top] 378 | [--activity-reorder-to-front] [--activity-reset-task-if-needed] 379 | [--activity-single-top] [--activity-clear-task] 380 | [--activity-task-on-home] [--activity-match-external] 381 | [--receiver-registered-only] [--receiver-replace-pending] 382 | [--receiver-foreground] [--receiver-no-abort] 383 | [--receiver-include-background] 384 | [--selector] 385 | [ | | ] 386 | -------------------------------------------------------------------------------- /apktool/smali.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | /////////////////////////////////////////........................ 6 | // ........................ 7 | // The Android Smali File Searcher ........................ 8 | // ........................ 9 | // @Author Shouheng Wang ........................ 10 | // ........................ 11 | /////////////////////////////////////////........................ 12 | ''' 13 | 14 | import os, sys, time, logging 15 | from typing import List 16 | sys.path.insert(0, '../') 17 | import global_config 18 | from files.textfiles import read_text 19 | from files.jsonfiles import write_json 20 | 21 | class SmaliSearcherConfiguration: 22 | '''The smali searcher configuration.''' 23 | def __init__(self) -> None: 24 | self.package = '' 25 | # The methods to search for example 'Ljava/lang/StringBuilder;->()V', or keyword to search. 26 | self.keywords = [] 27 | self.traceback = True 28 | self.result_store_path = "." 29 | self.traceback_generation = 5 30 | self.start_time = int(time.time()) 31 | 32 | def __str__(self) -> str: 33 | return "SmaliSearcherConfiguration [%s][%s][%s][%s][%s]" \ 34 | % (self.package, self.keywords, self.methods, \ 35 | str(self.traceback), self.result_store_path) 36 | 37 | def cost_time(self) -> str: 38 | '''Return and calculate the cost time.''' 39 | cost = int(time.time()) - self.start_time 40 | return "%d:%d" % (cost/60, cost%60) 41 | 42 | class SmaliMethod: 43 | '''Smali method wrapper class.''' 44 | def __init__(self) -> None: 45 | self.method_name = '' 46 | self.private = True 47 | self.path = '' 48 | self.traceback_count = 0 49 | self.pattern = '' 50 | 51 | def prepare_for_traceback(self, configuration: SmaliSearcherConfiguration): 52 | '''Prepare the smali method.''' 53 | # To avoid multiple calculation. 54 | if self.pattern != '': 55 | return 56 | # Calculate method pattern. 57 | h_index = self.path.find(configuration.package) 58 | r_index = self.path.find(".smali") 59 | if h_index >= 0 and r_index >= 0: 60 | cls = self.path[h_index:r_index] 61 | n_index = self.method_name.rfind(' ') 62 | name = self.method_name[n_index+1:] 63 | self.pattern = "L%s;->%s" % (cls, name) 64 | else: 65 | logging.error("Illegal traceback method: %s" % str(self)) 66 | 67 | def calculate_pattern(self, configuration: SmaliSearcherConfiguration): 68 | '''Calculate pattern.''' 69 | self.prepare_for_traceback(configuration) 70 | 71 | def __str__(self) -> str: 72 | return "SmaliMethod [%s][%s][%s][%s][%d]" % (str(self.private), \ 73 | self.method_name, self.path, self.pattern, self.traceback_count) 74 | 75 | class SmaliSercherResult: 76 | '''The smali searcher result.''' 77 | def __init__(self) -> None: 78 | self.keywords = {} 79 | self.methods = {} 80 | 81 | def prepare(self, configuration: SmaliSearcherConfiguration): 82 | '''Prepare the result object.''' 83 | for keyword in configuration.keywords: 84 | self.keywords[keyword] = [] 85 | 86 | def to_json(self, configuration: SmaliSearcherConfiguration): 87 | '''Transfer object to json.''' 88 | json_obj = { 89 | "keywords": {}, 90 | "methods": {} 91 | } 92 | # Prepare keywords json map. 93 | for k, items in self.keywords.items(): 94 | for item in items: 95 | item.calculate_pattern(configuration) 96 | if k not in json_obj["keywords"]: 97 | json_obj["keywords"][k] = [] 98 | json_obj["keywords"][k].append(item.pattern) 99 | # Prepare methods json map. 100 | for k, items in self.methods.items(): 101 | for item in items: 102 | item.calculate_pattern(configuration) 103 | if k not in json_obj["methods"]: 104 | json_obj["methods"][k] = [] 105 | json_obj["methods"][k].append(item.pattern) 106 | # Return json object. 107 | return json_obj 108 | 109 | def to_methods(self, configuration: SmaliSearcherConfiguration): 110 | '''Get all methods for json output.''' 111 | json_obj = [] 112 | patterns = [] 113 | for items in self.methods.values(): 114 | for item in items: 115 | item.calculate_pattern(configuration) 116 | if item.pattern not in patterns: 117 | patterns.append(item.pattern) 118 | json_obj.append(item.pattern) 119 | return json_obj 120 | 121 | def search_smali(dir: str, configuration: SmaliSearcherConfiguration=None): 122 | ''' 123 | Search in given directory. 124 | - dir: the directory for smali files, such as 'workspace_1637821369/smali_mix' 125 | - configuration: the smali searcher configuration 126 | ''' 127 | # Filt by package name. 128 | filted_dirs = _filt_by_packages(dir, configuration) 129 | logging.info("After filt by package: " + str(filted_dirs)) 130 | # Search in smali files by depth visit. 131 | for filted_dir in filted_dirs: 132 | search_by_depth_visit(filted_dir, configuration) 133 | 134 | def search_by_depth_visit(dir: str, configuration: SmaliSearcherConfiguration=None): 135 | '''Search keywords by depth visit.''' 136 | result = SmaliSercherResult() 137 | result.prepare(configuration) 138 | visits = [dir] 139 | searched = 0 140 | while len(visits) > 0 and _anything_need_to_search(configuration): 141 | visit = visits.pop() 142 | if os.path.exists(visit): 143 | for f in os.listdir(visit): 144 | path = os.path.join(visit, f) 145 | if os.path.isdir(path): 146 | visits.append(path) 147 | elif not _should_ignore_given_file(path): 148 | searched = searched + 1 149 | print(" >>> Searching [%d] under [%s] " % (searched, path), end = '\r') 150 | _search_under_given_file(path, result, configuration) 151 | # Traceback usages: find where these methods all called. 152 | if configuration.traceback: 153 | _traceback_keyword_usages(dir, result, searched, configuration) 154 | # Write searched result to json file. 155 | _write_result_to_json(result, configuration) 156 | 157 | def transfer_method(package: str, cls: str, method: str) -> str: 158 | ''' 159 | Transfer method of given class from code style to smali call style. 160 | For example, to transfer method of pacakge 'java.lang', class name 'StringBuilder' 161 | and method name 'StringBuilder()' to 'Ljava/lang/StringBuilder;->()V'. 162 | TODO complete the transformation method later. 163 | ''' 164 | f_package = package.replace('.', '/') 165 | return 'L%s/%s;->' % (f_package, cls) 166 | 167 | def _filt_by_packages(dir: str, configuration: SmaliSearcherConfiguration=None) -> List[str]: 168 | '''Filt directories by package name.''' 169 | dirs = [] 170 | # All directories are valid under given directory if the package is not configured. 171 | if configuration is None or len(configuration.package) == 0: 172 | dirs.append(dir) 173 | return dirs 174 | # Filt directories by package name. 175 | package = configuration.package.replace(".", "/") 176 | visits = [dir] 177 | while len(visits) > 0: 178 | visit = visits.pop() 179 | if os.path.exists(visit): 180 | for f in os.listdir(visit): 181 | path = os.path.join(visit, f) 182 | # Need to remove prefix to match the package name. 183 | valid = path.removeprefix(dir + "/") 184 | if os.path.isdir(path) and package.startswith(valid): 185 | if package == valid: 186 | dirs.append(path) 187 | else: 188 | visits.append(path) 189 | return dirs 190 | 191 | def _write_result_to_json(result: SmaliSercherResult, configuration: SmaliSearcherConfiguration=None): 192 | '''Write result to json file.''' 193 | version = int(time.time()) 194 | # Write methods mapping file. 195 | jsob_obj = result.to_json(configuration) 196 | f_name = "results_%d/smali_mappings.json" % version 197 | write_json(f_name, jsob_obj) 198 | # Write methods json file. 199 | jsob_obj = result.to_methods(configuration) 200 | f_name = "results_%d/smali_methods.json" % version 201 | write_json(f_name, jsob_obj) 202 | # Write method stack json file. 203 | jsob_obj = _compose_method_stacktrace(result) 204 | f_name = "results_%d/smali_stacks.json" % version 205 | write_json(f_name, jsob_obj) 206 | 207 | def _compose_method_stacktrace(result: SmaliSercherResult): 208 | '''Compose method stacktrace.''' 209 | json_obj = {} 210 | # Prepare keywords json map. 211 | for k, items in result.keywords.items(): 212 | for item in items: 213 | item.calculate_pattern(configuration) 214 | if k not in json_obj: 215 | json_obj[k] = [] 216 | json_obj[k].append(item.pattern) 217 | patterns = [] 218 | # Prepare methods json map. 219 | for k, items in result.methods.items(): 220 | for item in items: 221 | item.calculate_pattern(configuration) 222 | if item.pattern not in patterns: 223 | patterns.append(item.pattern) 224 | if item.pattern not in json_obj: 225 | json_obj[item.pattern] = [] 226 | json_obj[item.pattern].append(item.pattern) 227 | if k not in json_obj: 228 | json_obj[item.pattern].append(k) 229 | else: 230 | json_obj[item.pattern].extend(json_obj[k]) 231 | # Return json object. 232 | return json_obj 233 | 234 | def _anything_need_to_search(configuration: SmaliSearcherConfiguration=None) -> bool: 235 | '''To judge is there anything necessary to search.''' 236 | return len(configuration.keywords) > 0 237 | 238 | def _should_ignore_given_file(path: str, configuration: SmaliSearcherConfiguration=None) -> bool: 239 | ''' 240 | Should ignore given file. Ignore given file to accelerate search progress. 241 | Add more judgements for yourself to custom this operation. 242 | ''' 243 | parts = os.path.split('/') 244 | if len(parts) > 0: 245 | f_name = parts[len(parts)-1] 246 | if f_name.startswith("R$"): 247 | return True 248 | return False 249 | 250 | def _search_under_given_file(path: str, result: SmaliSercherResult, configuration: SmaliSearcherConfiguration=None): 251 | '''Search under given file.''' 252 | content = read_text(path) 253 | _search_keyword_under_given_file(path, content, result, configuration) 254 | 255 | def _search_keyword_under_given_file(path: str, content: str, result: SmaliSercherResult, configuration: SmaliSearcherConfiguration=None): 256 | '''Search keyword under given file based on text search.''' 257 | lines = content.split("\n") 258 | for keyword in configuration.keywords: 259 | if content.find(keyword) > 0: 260 | method_region = False 261 | for line in lines: 262 | # Found method region. 263 | if line.startswith(".method"): 264 | method = SmaliMethod() 265 | method_region = True 266 | method.path = path 267 | method.method_name = line.removeprefix(".method").strip() 268 | method.private = line.startswith(".method private") 269 | elif line.startswith(".end method"): 270 | method_region = False 271 | # Add method to result. 272 | if line.find(keyword) > 0: 273 | if method_region and method.method_name != '': 274 | logging.debug("Found keyword usage: %s" % str(method)) 275 | result.keywords[keyword].append(method) 276 | if method.private: 277 | _search_private_method_usage_under_current_file(path, content, method, result, configuration) 278 | else: 279 | logging.error("Found one isolate keyword in [%s][%s]" % (path, line)) 280 | 281 | def _search_private_method_usage_under_current_file(path: str, content: str, to_search: SmaliMethod, result: SmaliSercherResult, configuration: SmaliSearcherConfiguration=None): 282 | '''Search private method usage in current file.''' 283 | to_search.calculate_pattern(configuration) 284 | logging.debug("Search private method under current file for: %s" % (str(to_search.pattern))) 285 | lines = content.split("\n") 286 | method_region = False 287 | for line in lines: 288 | # Find method region. 289 | if line.startswith(".method"): 290 | method = SmaliMethod() 291 | method_region = True 292 | method.path = path 293 | method.method_name = line.removeprefix(".method").strip() 294 | method.private = line.startswith(".method private") 295 | elif line.startswith(".end method"): 296 | method_region = False 297 | # Add method to result. 298 | if line.find(to_search.pattern) > 0: 299 | if method_region and method.method_name != '': 300 | # Clear method region flag. 301 | method_region = False 302 | logging.debug("Found method usage: %s" % str(method)) 303 | if result.methods.get(to_search.pattern) == None: 304 | result.methods[to_search.pattern] = [] 305 | result.methods[to_search.pattern].append(method) 306 | else: 307 | logging.error("Found one isolate method usage in [%s][%s]" % (path, line)) 308 | 309 | def _traceback_keyword_usages(dir: str, result: SmaliSercherResult, total: int, configuration: SmaliSearcherConfiguration=None): 310 | '''Traceback keyword usages.''' 311 | # Prepare methods list for traceback. 312 | visit_methods = [] 313 | for methods in result.keywords.values(): 314 | for method in methods: 315 | if not method.private: 316 | method.prepare_for_traceback(configuration) 317 | visit_methods.append(method) 318 | for methods in result.methods.values(): 319 | for method in methods: 320 | if not method.private: 321 | method.prepare_for_traceback(configuration) 322 | visit_methods.append(method) 323 | # Continusly visit the tree. 324 | circle = 0 325 | while len(visit_methods) > 0 and circle < configuration.traceback_generation: 326 | visits = [dir] 327 | searched = 0 328 | circle = circle+1 329 | # Add the max traceback generation judgement to avoid traceback too much. 330 | while len(visits) > 0 and len(visit_methods) > 0: 331 | visit = visits.pop() 332 | if os.path.exists(visit): 333 | for f in os.listdir(visit): 334 | path = os.path.join(visit, f) 335 | if os.path.isdir(path): 336 | visits.append(path) 337 | elif not _should_ignore_given_file(path): 338 | searched = searched + 1 339 | print(" >>> Traceback [%s][%d][%d][%d][%d] under [%s] " % (configuration.cost_time(), circle, searched, len(visit_methods), total, path), end = '\r') 340 | _traceback_methods_usages(path, visit_methods, result, total, configuration) 341 | 342 | def _connect_visit_methods(methods: List[SmaliMethod]) -> str: 343 | '''Connect visit methods.''' 344 | ret = '' 345 | for method in methods: 346 | ret = ret + ' ' + str(method) 347 | return ret 348 | 349 | def _traceback_methods_usages(path: str, methods: List[SmaliMethod], result: SmaliSercherResult, total: int, configuration: SmaliSearcherConfiguration=None): 350 | '''Traceback methods usages.''' 351 | content = read_text(path) 352 | lines = content.split("\n") 353 | visit_methods = [] 354 | visit_methods.extend(methods) 355 | private_visited_pattern = [] 356 | for visit in visit_methods: 357 | # Increase traceback count and remove if it was visited a circle. 358 | visit.traceback_count = visit.traceback_count+1 359 | if visit.traceback_count > total: 360 | methods.remove(visit) 361 | logging.debug("Remove traceback method %s" % str(visit)) 362 | # Skip 363 | continue 364 | if visit.pattern not in result.methods: 365 | result.methods[visit.pattern] = [] 366 | # Find usage for method. 367 | if content.find(visit.pattern) > 0: 368 | method_region = False 369 | for line in lines: 370 | # Find method region. 371 | if line.startswith(".method"): 372 | method = SmaliMethod() 373 | method_region = True 374 | method.path = path 375 | method.method_name = line.removeprefix(".method").strip() 376 | method.private = line.startswith(".method private") 377 | elif line.startswith(".end method"): 378 | method_region = False 379 | # Add method to result. Should ignore method signature line. 380 | if line.find(visit.pattern) > 0: 381 | if method_region and method.method_name != '': 382 | # Clear method region state: if the 'visit' was called twice or more in current method, 383 | # only keep one method record. 384 | method_region = False 385 | logging.debug("Found traceback method usage of %s in %s" % (str(visit), str(method))) 386 | result.methods[visit.pattern].append(method) 387 | # Add to traceback methods to search continusly if it's not private. 388 | if not method.private: 389 | method.prepare_for_traceback(configuration) 390 | # Add judgement to avoid visit the same method twice. 391 | if method.pattern not in result.methods: 392 | methods.append(method) 393 | # Anyway, add one map to methods if the pattern is visited. Used to avoid multiple times visit. 394 | result.methods[method.pattern] = [] 395 | else: 396 | # Need to avoid same private method visited multiple times. 397 | if method.method_name not in private_visited_pattern: 398 | private_visited_pattern.append(method.method_name) 399 | # TODO methods calling private method might need to be added to 'methods' to traceback. 400 | _search_private_method_usage_under_current_file(path, content, method, result, configuration) 401 | else: 402 | logging.error("Found one isolate method usage in [%s][%s]" % (path, line)) 403 | 404 | if __name__ == "__main__": 405 | '''Program entrance.''' 406 | global_config.config_logging('../log/app.log') 407 | configuration = SmaliSearcherConfiguration() 408 | # print(configuration.traceback) 409 | # print(configuration) 410 | # configuration.methods = [] 411 | search_smali("workspace_1637821369/smali_mix", configuration) 412 | # search_under_smali("workspace_1637821369/smali_classes3", configuration) 413 | # search_by_depth_visit('workspace_1637821369/smali_mix/com/ 414 | /cloudmusic', configuration) 415 | # method = SmaliMethod() 416 | # method.method_name = "private static getChannel(I)Ljava/lang/String;" 417 | # method.prepare_for_traceback(configuration) 418 | # print(str(method)) 419 | # list = [1, 2, 3, 4] 420 | # for item in list: 421 | # list.append(5) 422 | # print(list) 423 | --------------------------------------------------------------------------------