Compare commits
	
		
			4 commits
		
	
	
		
			85923ac1a5
			...
			8a7b95beee
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8a7b95beee | |||
|   | f2ada0e2fe | ||
|   | e8ffddf93f | ||
| 9e065cd638 | 
					 23 changed files with 324 additions and 17899 deletions
				
			
		
							
								
								
									
										7
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,11 +1,8 @@ | ||||||
| build/ | build/ | ||||||
| source-apk/*.apk | source-apk/*.apk | ||||||
| keystores/*.keystore | keystores/*.keystore | ||||||
| tools/* |  | ||||||
| !tools/smali-2.5.2.jar |  | ||||||
| !tools/apktool_2.9.3.jar |  | ||||||
| !tools/apksigner.jar |  | ||||||
| !tools/d8.jar |  | ||||||
| 
 | 
 | ||||||
| .idea | .idea | ||||||
| *.iml | *.iml | ||||||
|  | 
 | ||||||
|  | __pycache__ | ||||||
							
								
								
									
										15
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | ||||||
|  | { | ||||||
|  |     // Use IntelliSense to learn about possible attributes. | ||||||
|  |     // Hover to view descriptions of existing attributes. | ||||||
|  |     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||||
|  |     "version": "0.2.0", | ||||||
|  |     "configurations": [ | ||||||
|  |         { | ||||||
|  |             "name": "Python Debugger: Current File", | ||||||
|  |             "type": "debugpy", | ||||||
|  |             "request": "launch", | ||||||
|  |             "program": "test.py", | ||||||
|  |             "console": "integratedTerminal" | ||||||
|  |         } | ||||||
|  |     ] | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.vscode/settings.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | { | ||||||
|  | } | ||||||
							
								
								
									
										234
									
								
								build.py
									
										
									
									
									
								
							
							
						
						
									
										234
									
								
								build.py
									
										
									
									
									
								
							|  | @ -1,234 +0,0 @@ | ||||||
| import sys |  | ||||||
| import shutil |  | ||||||
| import os |  | ||||||
| from pathlib import Path |  | ||||||
| import subprocess |  | ||||||
| 
 |  | ||||||
| class BuildError(Exception): |  | ||||||
|     def __init__(self): |  | ||||||
|         super.__init__() |  | ||||||
| 
 |  | ||||||
| def getDexName(smaliName): |  | ||||||
|     if smaliName == 'smali': |  | ||||||
|         return 'classes.dex' |  | ||||||
|     elif smaliName.startswith('smali_classes'): |  | ||||||
|         return 'classes' + smaliName[13:] + '.dex' |  | ||||||
|     print("Not a smali name") |  | ||||||
|     raise BuildError() |  | ||||||
| 
 |  | ||||||
| class Task: |  | ||||||
|     def clean(): |  | ||||||
|         shutil.rmtree('build') |  | ||||||
|      |  | ||||||
|     def extractApk(): |  | ||||||
|         if Path('build/extracted').exists(): |  | ||||||
|             return |  | ||||||
|         Path('build').mkdir(parents=True, exist_ok=True) |  | ||||||
| 
 |  | ||||||
|         apk = list(filter(lambda f: f.endswith('.apk'), os.listdir('source-apk'))) |  | ||||||
|         if len(apk) == 0: |  | ||||||
|             print('Please place the APK inside the source-apk directory.') |  | ||||||
|             raise BuildError() |  | ||||||
|         elif len(apk) > 1: |  | ||||||
|             print('More than one apk found in source-apk. Please remove all but one.') |  | ||||||
|             raise BuildError() |  | ||||||
| 
 |  | ||||||
|         # Extract the apk |  | ||||||
|         print("Extracting APK...") |  | ||||||
|         subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'd', 'source-apk/' + apk[0], '-o', 'build/extracted']) |  | ||||||
| 
 |  | ||||||
|     def copySmali(): |  | ||||||
|         Task.extractApk() |  | ||||||
| 
 |  | ||||||
|         Path('build/smali').mkdir(parents=True, exist_ok=True) |  | ||||||
|         # Copy smali classes that haven't been yet |  | ||||||
|         for file in os.listdir('build/extracted'): |  | ||||||
|             if file != 'smali' and not file.startswith('smali_classes'): |  | ||||||
|                 continue |  | ||||||
|              |  | ||||||
|             # Skip if already exists |  | ||||||
|             if Path(f'build/smali/{file}').exists(): |  | ||||||
|                 continue |  | ||||||
| 
 |  | ||||||
|             print(f'Copying {file}...') |  | ||||||
|             shutil.copytree(f'build/extracted/{file}', f'build/smali/{file}') |  | ||||||
| 
 |  | ||||||
|         # Copy from src |  | ||||||
|         print('Copying ykit smali...') |  | ||||||
|         shutil.copytree('src/smali', 'build/smali', dirs_exist_ok=True) |  | ||||||
| 
 |  | ||||||
|     def compileSmali(): |  | ||||||
|         Task.copySmali() |  | ||||||
| 
 |  | ||||||
|         Path('build/dex').mkdir(parents=True, exist_ok=True) |  | ||||||
| 
 |  | ||||||
|         def shouldCompileSmali(file, dex): |  | ||||||
|             src = f'build/smali/{file}' |  | ||||||
|             ykitsrc = f'src/smali/{file}' |  | ||||||
|             dst = f'build/dex/{dex}' |  | ||||||
| 
 |  | ||||||
|             # If dest doesn't exist, always build |  | ||||||
|             if not Path(dst).exists(): |  | ||||||
|                 return True |  | ||||||
| 
 |  | ||||||
|             # Check if ykit src dir exists for it, if not then don't recompile |  | ||||||
|             if not Path(ykitsrc).exists(): |  | ||||||
|                 return False |  | ||||||
| 
 |  | ||||||
|             # Get latest file timestamp in ykitsrc |  | ||||||
|             files = list(Path(ykitsrc).rglob('*.smali')) |  | ||||||
|             files = [os.path.getmtime(x) for x in files] |  | ||||||
|             latest = max(files) |  | ||||||
| 
 |  | ||||||
|             dsttime = os.path.getmtime(dst) |  | ||||||
| 
 |  | ||||||
|             # If it is newer than the dest, then do recompile |  | ||||||
|             if latest > dsttime: |  | ||||||
|                 return True |  | ||||||
| 
 |  | ||||||
|             # Otherwise, don't |  | ||||||
|             return False |  | ||||||
| 
 |  | ||||||
|         for file in os.listdir('build/smali'): |  | ||||||
|             dex = getDexName(file) |  | ||||||
| 
 |  | ||||||
|             if not shouldCompileSmali(file, dex): |  | ||||||
|                 print(f"Skipping {file}...") |  | ||||||
|                 continue |  | ||||||
| 
 |  | ||||||
|             print(f"Compiling {file}...") |  | ||||||
|             subprocess.run(['java', '-jar', 'tools/smali-2.5.2.jar', 'a', f'build/smali/{file}', '-o', f'build/dex/{dex}']) |  | ||||||
| 
 |  | ||||||
|     def genClassPath(): |  | ||||||
|         print("Generating java classpath from dex files...") |  | ||||||
|         for file in os.listdir('build/dex'): |  | ||||||
|             src = f'build/dex/{file}' |  | ||||||
|             dst = f'build/jar/{file[:-4]}.jar' |  | ||||||
|             srctime = os.path.getmtime(src) |  | ||||||
|             dsttime = 0 if not Path(dst).exists() else os.path.getmtime(dst) |  | ||||||
| 
 |  | ||||||
|             if srctime < dsttime: |  | ||||||
|                 print(f"Skipping {file}...") |  | ||||||
|                 continue |  | ||||||
| 
 |  | ||||||
|             print(f"Dex2jar {file}...") |  | ||||||
|             subprocess.run(['java', '-cp', 'tools/d2j/*', 'com.googlecode.dex2jar.tools.Dex2jarCmd', '-f', src, '-o', dst]) |  | ||||||
| 
 |  | ||||||
|     def compileJava(): |  | ||||||
|         Task.genClassPath() |  | ||||||
| 
 |  | ||||||
|         sep = ';' if os.name == 'nt' else ':' |  | ||||||
|         javaFiles = [str(x) for x in Path('src/java').rglob('*.java')] |  | ||||||
|         classPath = sep.join([str(x) for x in Path('build/jar').rglob('*.jar')]) |  | ||||||
| 
 |  | ||||||
|         # Get latest java file timestamp |  | ||||||
|         files = list(Path('src/java').rglob('*.java')) |  | ||||||
|         files = [os.path.getmtime(x) for x in files] |  | ||||||
|         latest = max(files) |  | ||||||
| 
 |  | ||||||
|         # Get latest dex time or 0 |  | ||||||
|         latest2 = 0 if not Path('build/java_dex/classes.dex').exists() else os.path.getmtime('build/java_dex/classes.dex') |  | ||||||
| 
 |  | ||||||
|         # If dex file is newer than source, no need to do anything |  | ||||||
|         if latest2 > latest: |  | ||||||
|             print("Skipping java...") |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         print("Compiling java...") |  | ||||||
|         subprocess.run(['javac', '-cp', classPath, '-d', 'build/java_class', *javaFiles]) |  | ||||||
| 
 |  | ||||||
|         classFiles = [str(x) for x in Path('build/java_class').rglob('*.class')] |  | ||||||
| 
 |  | ||||||
|         Path('build/java_dex').mkdir(parents=True, exist_ok=True) |  | ||||||
|         subprocess.run(['java', '-cp', 'tools/d8.jar', 'com.android.tools.r8.D8', *classFiles, '--output', 'build/java_dex']) |  | ||||||
| 
 |  | ||||||
|     def mergeResources(): |  | ||||||
|         Task.compileSmali() |  | ||||||
|         Task.compileJava() |  | ||||||
| 
 |  | ||||||
|         # Copy original resources |  | ||||||
|         # on first run only |  | ||||||
|         if not Path('build/merged').exists(): |  | ||||||
|             print('Copying extracted resources...') |  | ||||||
|             shutil.copytree('build/extracted', 'build/merged', dirs_exist_ok=True, ignore=shutil.ignore_patterns('smali', 'smali_classes*')) |  | ||||||
| 
 |  | ||||||
|         print("Copying ykit resources...") |  | ||||||
|         shutil.copytree('src/resources', 'build/merged', dirs_exist_ok=True) |  | ||||||
| 
 |  | ||||||
|         print("Copying compiled dex files...") |  | ||||||
|         shutil.copytree('build/dex', 'build/merged', dirs_exist_ok=True) |  | ||||||
|         shutil.copy('build/java_dex/classes.dex', 'build/merged/classes7.dex') # TODO: Unhardcode this |  | ||||||
| 
 |  | ||||||
|     def buildApk(): |  | ||||||
|         Task.mergeResources() |  | ||||||
| 
 |  | ||||||
|         debugFlag = ('-d',) if ('d' in ''.join(sys.argv[2:])) else () |  | ||||||
| 
 |  | ||||||
|         print("Building apk...") |  | ||||||
|         subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'b', *debugFlag, 'build/merged', '-o', 'build/apk/tumblr-ykit-unsigned.apk']) |  | ||||||
| 
 |  | ||||||
|     def alignApk(): |  | ||||||
|         Task.buildApk() |  | ||||||
| 
 |  | ||||||
|         print('Aligning apk...') |  | ||||||
|         subprocess.run(['tools/zipalign', '-p', '-f', '-v', '4', 'build/apk/tumblr-ykit-unsigned.apk', 'build/apk/tumblr-ykit.apk']) |  | ||||||
| 
 |  | ||||||
|     def debugKey(): |  | ||||||
|         if Path('keystores/debug.keystore').exists(): |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         print("Debug keystore does not exist, generating...") |  | ||||||
|         subprocess.run(['keytool', '-genkey', '-v', '-keystore', 'keystores/debug.keystore', '-alias', 'alias_name', '-keyalg', 'RSA', '-keysize', '2048', '-validity', '10000', '-storepass', '123456', '-keypass', '123456', '-dname', 'CN=, OU=, O=, L=, ST=, C=']) |  | ||||||
| 
 |  | ||||||
|     def signApk(): |  | ||||||
|         Task.alignApk() |  | ||||||
|         Task.debugKey() |  | ||||||
| 
 |  | ||||||
|         debugFlag = 'd' in ''.join(sys.argv[2:]) |  | ||||||
|         releaseFlag = 'r' in ''.join(sys.argv[2:]) |  | ||||||
| 
 |  | ||||||
|         if not (debugFlag or releaseFlag): |  | ||||||
|             print("Neither debug nor release flag was specified so the apk will not be signed.") |  | ||||||
|             return |  | ||||||
| 
 |  | ||||||
|         if releaseFlag: |  | ||||||
|             if not Path('keystores/release.keystore').exists(): |  | ||||||
|                 print("release.keystore is missing from keystores directory") |  | ||||||
|                 return |  | ||||||
|              |  | ||||||
|             keystoreOpts = ('--ks', 'keystores/release.keystore',) |  | ||||||
|         else: |  | ||||||
|             Task.debugKey() |  | ||||||
|             keystoreOpts = ('--ks', 'keystores/debug.keystore', '--ks-pass', 'pass:123456',) |  | ||||||
| 
 |  | ||||||
|         print('Signing apk...') |  | ||||||
|         subprocess.run(['java', '-jar', './tools/apksigner.jar', 'sign', *keystoreOpts, 'build/apk/tumblr-ykit.apk']) |  | ||||||
|         print("Successfully signed apk in 'build/apk/tumblr-ykit.apk' using " + ('release' if releaseFlag else 'debug') + ' keystore') |  | ||||||
| 
 |  | ||||||
|     def deployToAndroidStudio(): |  | ||||||
|         projDir = Path.home() / 'ApkProjects/tumblr-ykit' |  | ||||||
|         if not projDir.exists(): |  | ||||||
|             print("No project named 'tumblr-ykit' in ~/ApkProjects/") |  | ||||||
| 
 |  | ||||||
|         shutil.copy('build/apk/tumblr-ykit.apk', projDir / 'tumblr-ykit.apk') |  | ||||||
|         print("Deployed to Android Studio") |  | ||||||
| 
 |  | ||||||
| def main(): |  | ||||||
| 
 |  | ||||||
|     match sys.argv[1]: |  | ||||||
|         case 'clean': |  | ||||||
|             Task.clean() |  | ||||||
|         case 'extract': |  | ||||||
|             Task.extractApk() |  | ||||||
|         case 'assemble': |  | ||||||
|             Task.buildApk() |  | ||||||
|         case 'build': |  | ||||||
|             Task.signApk() |  | ||||||
|              |  | ||||||
|             if ('a' in ''.join(sys.argv[2:])): |  | ||||||
|                 Task.deployToAndroidStudio() |  | ||||||
|         case 'genclasspath': |  | ||||||
|             Task.genClassPath() |  | ||||||
| 
 |  | ||||||
| if __name__ == '__main__': |  | ||||||
|     main() |  | ||||||
							
								
								
									
										2
									
								
								build.sh
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								build.sh
									
										
									
									
									
								
							|  | @ -1 +1 @@ | ||||||
| python build.py "$@" | python buildtool/build.py "$@" | ||||||
							
								
								
									
										5
									
								
								buildtool/build.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								buildtool/build.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | # from task.extract import extract | ||||||
|  | from task.patch_resources import patch_resources | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     _ = patch_resources() | ||||||
							
								
								
									
										44
									
								
								buildtool/polly/grammar.lark
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								buildtool/polly/grammar.lark
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | ||||||
|  | start: (patch)* | ||||||
|  | patch: file_declaration instruction* | ||||||
|  | 
 | ||||||
|  | file_declaration: "file" string | ||||||
|  | 
 | ||||||
|  | ?string: STRING | RAW_STRING | LONG_STRING | ||||||
|  | 
 | ||||||
|  | ?instruction: insert | ||||||
|  |             | preg_replace | ||||||
|  |             | define_anchor | ||||||
|  | 
 | ||||||
|  | insert: "insert" location string | ||||||
|  | 
 | ||||||
|  | preg_replace: "preg_replace" range PATTERN string | ||||||
|  | 
 | ||||||
|  | define_anchor: "define_anchor" anchor affix PATTERN | ||||||
|  |              | "define_anchor" range_anchor "range" PATTERN | ||||||
|  | 
 | ||||||
|  | ?affix: "before" | "after" | ||||||
|  | 
 | ||||||
|  | ?location: LINE_NUMBER | ||||||
|  |          | CHAR_INDEX | ||||||
|  |          | LINE_COLUMN | ||||||
|  |          | anchor | ||||||
|  | 
 | ||||||
|  | range: "(" location "-" location ")" | ||||||
|  |      | range_anchor | ||||||
|  | 
 | ||||||
|  | anchor: "@" IDENTIFIER | ||||||
|  | range_anchor: "@$" IDENTIFIER | ||||||
|  | 
 | ||||||
|  | STRING: /"([^"\\]|\\.)*"/ | ||||||
|  | RAW_STRING: /'[^']*'/ | ||||||
|  | LONG_STRING: /<<\s*(?P<terminator>[^\n]+)\n.*\n(?P=terminator)/s | ||||||
|  | LINE_NUMBER: /ln\d+/ | ||||||
|  | CHAR_INDEX: /ch\d+/ | ||||||
|  | LINE_COLUMN: /ln\d+c\d+/ | ||||||
|  | IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/ | ||||||
|  | PATTERN: /\/([^\/\\]|\\.)*\// | ||||||
|  | 
 | ||||||
|  | COMMENT: /#[^\r\n]+/ | ||||||
|  | 
 | ||||||
|  | %ignore /[\t \f\n]+/ | ||||||
|  | %ignore COMMENT | ||||||
							
								
								
									
										63
									
								
								buildtool/polly/parser.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								buildtool/polly/parser.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | ||||||
|  | from lark import Lark | ||||||
|  | from lark.lexer import Token | ||||||
|  | from lark.tree import Branch | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | import re | ||||||
|  | 
 | ||||||
|  | def parse_string(token: Token): | ||||||
|  |     if token.type == 'STRING': | ||||||
|  |         string = token.value[1:-1] | ||||||
|  |         string = re.sub(r"\\n","\n", string) | ||||||
|  |         string = re.sub(r"\\t","\t", string) | ||||||
|  |         string = re.sub(r"\\r","\r", string) | ||||||
|  |         string = re.sub(r"\\(.)",r"\1", string) | ||||||
|  |         return string | ||||||
|  |     elif token.type == 'RAW_STRING': | ||||||
|  |         string = token.value[1:-1] | ||||||
|  |         return string | ||||||
|  |     elif token.type == 'LONG_STRING': | ||||||
|  |         string = re.match(re.compile(r"<<\s*(?P<terminator>[^\n]+)\n(.*)\n(?P=terminator)", re.MULTILINE + re.DOTALL),token.value) | ||||||
|  |         assert string is not None | ||||||
|  |         string = string.group(2) | ||||||
|  |         return string | ||||||
|  |      | ||||||
|  | def parse_location(location: Token): | ||||||
|  |     assert isinstance(location.value, str) | ||||||
|  |     m = re.match(r"ln([0-9]+)(?:c([0-9]+))?", location.value) | ||||||
|  |     if m: | ||||||
|  |         ln, col = m.groups() | ||||||
|  |         return {"type": "lncol", "line": int(ln), "column": int(col) if col else 0} | ||||||
|  |      | ||||||
|  |     m = re.match(r"ch([0-9]+)", location.value) | ||||||
|  |     if m: | ||||||
|  |         ch = m.groups()[0] | ||||||
|  |         return {"type": "char", "index": int(ch)} | ||||||
|  | 
 | ||||||
|  |     raise RuntimeError("Cannot parse location") | ||||||
|  | 
 | ||||||
|  | def parse_patch(branch: Branch[Token], mtime: float): | ||||||
|  |     # First instruction is always file declaration | ||||||
|  |     target_file = parse_string(branch.children[0].children[0]) # pyright: ignore[reportUnknownMemberType, reportArgumentType] | ||||||
|  | 
 | ||||||
|  |     actions = [] | ||||||
|  | 
 | ||||||
|  |     for inst in branch.children[1:]: | ||||||
|  |         match inst.data: | ||||||
|  |             case "insert": | ||||||
|  |                 actions.append({"type": "insert", "at": parse_location(inst.children[0]), "content": parse_string(inst.children[1])}) | ||||||
|  |                 # print(f"Inserting {parse_string(inst.children[1])} at {inst.children[0]} in {target_file}") | ||||||
|  | 
 | ||||||
|  |     return {"target": target_file, "actions": actions, "timestamp": mtime} | ||||||
|  | 
 | ||||||
|  | def parse_patch_file(file: str): | ||||||
|  |     lark = Lark.open('grammar.lark', rel_to=__file__) | ||||||
|  | 
 | ||||||
|  |     mtime = os.path.getmtime(file) | ||||||
|  | 
 | ||||||
|  |     with open(file, 'r') as f: | ||||||
|  |         result = lark.parse(f.read()) | ||||||
|  |         patches = [parse_patch(patch, mtime) for patch in result.children] | ||||||
|  |          | ||||||
|  |     return patches | ||||||
							
								
								
									
										71
									
								
								buildtool/polly/patcher.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								buildtool/polly/patcher.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | ||||||
|  | from .parser import parse_patch_file | ||||||
|  | 
 | ||||||
|  | from pathlib import Path | ||||||
|  | 
 | ||||||
|  | from task.util import * | ||||||
|  | 
 | ||||||
|  | def resolve_position(location, src: str): | ||||||
|  |     if location["type"] == "char": | ||||||
|  |         return location["index"] | ||||||
|  |     elif location["type"] == "lncol": | ||||||
|  |         j = 0 | ||||||
|  |         for i, x in enumerate(src.split('\n')): | ||||||
|  |             if (i+1) == location["line"]: | ||||||
|  |                 if location["column"] > len(x): | ||||||
|  |                     print(f"Cannot insert at line {location['line']} column {location['column']}. The line is not that long.") | ||||||
|  |                     exit(-1) | ||||||
|  |                 return j + location["column"] | ||||||
|  |             j += len(x) + 1 | ||||||
|  |              | ||||||
|  |         print(f"Cannot insert at line {location['line']}. The file is not long enough.") | ||||||
|  |         exit(-1) | ||||||
|  | 
 | ||||||
|  | def read_insertion(action, src): | ||||||
|  |     position = resolve_position(action["at"], src) | ||||||
|  |     return {"content": action["content"], "pos": position} | ||||||
|  | 
 | ||||||
|  | def process_patch_file(patch_file: str, check_date: bool = True): | ||||||
|  |     patches = parse_patch_file(patch_file) | ||||||
|  |      | ||||||
|  |     for patch in patches: | ||||||
|  |         target_src = EXTRACTED_DIR / patch["target"] | ||||||
|  |         target_dest = PATCHED_RSC_DIR / patch["target"] | ||||||
|  |          | ||||||
|  |         # Skip old patches | ||||||
|  |         if check_date and target_dest.exists() and target_dest.stat().st_mtime > patch["timestamp"]: | ||||||
|  |             print(f"Skipping patch {patch['target']}...") | ||||||
|  |             continue | ||||||
|  |          | ||||||
|  |         print(f"Patching {patch['target']} with {len(patch['actions'])} actions") | ||||||
|  |          | ||||||
|  |         # Read the source of the file | ||||||
|  |         with open(target_src, 'r') as f: | ||||||
|  |             src = f.read() | ||||||
|  |              | ||||||
|  |         # Resolve the actual char locations in the patches | ||||||
|  |         insertions = [read_insertion(action, src) for action in patch["actions"]] | ||||||
|  |         insertions = [x for x in insertions if x is not None] | ||||||
|  |          | ||||||
|  |         for insertion in insertions: | ||||||
|  |             src = src[:insertion["pos"]] + insertion["content"] + src[insertion["pos"]:] | ||||||
|  | 
 | ||||||
|  |             # Correct insertions that follow this one | ||||||
|  |             for i2 in insertions: | ||||||
|  |                 # Only update insertions that come *after* this one | ||||||
|  |                 if i2 == insertion or i2["pos"] < insertion["pos"]: | ||||||
|  |                     continue | ||||||
|  |                  | ||||||
|  |                 i2["pos"] += len(insertion["content"]) | ||||||
|  |                  | ||||||
|  |         # Write the changed output | ||||||
|  |         with open(target_dest, 'w') as f: | ||||||
|  |             f.write(src) | ||||||
|  |              | ||||||
|  | def process_all_patches(dir: Path | str, check_date: bool = True): | ||||||
|  |     dir = Path(dir) | ||||||
|  |      | ||||||
|  |     for (cd, _, files) in dir.walk(): | ||||||
|  |         for f in files: | ||||||
|  |             if f.endswith(".patch"): | ||||||
|  |                 print("Processing patch {}".format((cd / f).relative_to(dir))) | ||||||
|  |                 process_patch_file(str(cd / f), check_date=check_date) | ||||||
							
								
								
									
										25
									
								
								buildtool/task/extract.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								buildtool/task/extract.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | ||||||
|  | from .util import * | ||||||
|  | 
 | ||||||
|  | import subprocess | ||||||
|  | 
 | ||||||
|  | def find_apk(): | ||||||
|  |     apks = list(SOURCE_APK_DIR.glob("*.apk")) | ||||||
|  |     if len(apks) == 0: | ||||||
|  |         print("Source APK is missing from 'source-apk' directory. Please make sure to copy it to the right directory.") | ||||||
|  |         exit(-1) | ||||||
|  |     elif len(apks) > 1: | ||||||
|  |         print("Multiple APKs were found in the 'source-apk' directory. Please only place the correct APK there.") | ||||||
|  |         exit(-1) | ||||||
|  | 
 | ||||||
|  |     return apks[0] | ||||||
|  | 
 | ||||||
|  | def extract(): | ||||||
|  |     target_apk = find_apk() | ||||||
|  | 
 | ||||||
|  |     # Check if this task is necessary | ||||||
|  |     if EXTRACTED_DIR.exists() and EXTRACTED_DIR.stat().st_mtime > target_apk.stat().st_mtime: | ||||||
|  |         return "pass" | ||||||
|  |      | ||||||
|  |     # Extract the apk | ||||||
|  |     print("Extracting APK...") | ||||||
|  |     _ = subprocess.run(['java', '-jar', './tools/apktool_2.9.3.jar', 'd', str(target_apk.absolute()), '-o', EXTRACTED_DIR]) | ||||||
							
								
								
									
										23
									
								
								buildtool/task/fileutil.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								buildtool/task/fileutil.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | from pathlib import Path | ||||||
|  | import shutil | ||||||
|  | 
 | ||||||
|  | def merge_into(src: Path | str, dest: Path | str, check_date: bool = True): | ||||||
|  |     src = Path(src) | ||||||
|  |     dest = Path(dest) | ||||||
|  |      | ||||||
|  |     changed = False | ||||||
|  |      | ||||||
|  |     for (cd, _, files) in src.walk(): | ||||||
|  |         for file in files: | ||||||
|  |             src_file = cd / file | ||||||
|  |             dest_file = dest / src_file.relative_to(src) | ||||||
|  |              | ||||||
|  |             # Don't update if dest is newer than source | ||||||
|  |             if check_date and dest_file.exists() and src_file.stat().st_mtime < dest_file.stat().st_mtime: | ||||||
|  |                 continue | ||||||
|  |              | ||||||
|  |             dest_file.parent.mkdir(parents=True, exist_ok=True) | ||||||
|  |             _ = shutil.copyfile(src_file, dest_file) | ||||||
|  |             changed = True | ||||||
|  |              | ||||||
|  |     return changed | ||||||
							
								
								
									
										29
									
								
								buildtool/task/patch_resources.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								buildtool/task/patch_resources.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | from .util import * | ||||||
|  | from . import fileutil | ||||||
|  | 
 | ||||||
|  | import shutil | ||||||
|  | 
 | ||||||
|  | from polly.patcher import process_all_patches | ||||||
|  | 
 | ||||||
|  | # Task dependencies | ||||||
|  | from .extract import extract | ||||||
|  | 
 | ||||||
|  | def patch_resources(): | ||||||
|  |     _ = extract() | ||||||
|  |      | ||||||
|  |     first_time = False | ||||||
|  |      | ||||||
|  |     # Copy original resources only the first time | ||||||
|  |     if not PATCHED_RSC_DIR.exists(): | ||||||
|  |         print("Copying original resources...") | ||||||
|  |         shutil.copytree(EXTRACTED_DIR, PATCHED_RSC_DIR, ignore=shutil.ignore_patterns("smali*")) | ||||||
|  |         first_time = True | ||||||
|  |      | ||||||
|  |     s = fileutil.merge_into(SRC_RESOURCES_DIR, PATCHED_RSC_DIR, check_date=not first_time) | ||||||
|  |     if s: | ||||||
|  |         print("Copied custom resources from src/resources") | ||||||
|  |     else: | ||||||
|  |         print("Skipped copying resources from src/resources") | ||||||
|  |          | ||||||
|  |     # Patch other resources | ||||||
|  |     process_all_patches(SRC_PATCHES_DIR, check_date=not first_time) | ||||||
							
								
								
									
										11
									
								
								buildtool/task/util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								buildtool/task/util.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | from pathlib import Path | ||||||
|  | 
 | ||||||
|  | SOURCE_APK_DIR = Path("source-apk") | ||||||
|  | 
 | ||||||
|  | BUILD_DIR = Path("build") | ||||||
|  | EXTRACTED_DIR = BUILD_DIR / "extracted" | ||||||
|  | PATCHED_RSC_DIR = BUILD_DIR / "patched_resources" | ||||||
|  | 
 | ||||||
|  | SRC_DIR = Path("src") | ||||||
|  | SRC_RESOURCES_DIR = SRC_DIR / "resources" | ||||||
|  | SRC_PATCHES_DIR = SRC_DIR / "patches" | ||||||
							
								
								
									
										1
									
								
								msys.bat
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								msys.bat
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | C:\msys64\msys2_shell.cmd -here -defterm -no-start -full-path | ||||||
							
								
								
									
										16
									
								
								src/patches/public.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/patches/public.patch
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | file 'res/values/public.xml' | ||||||
|  | 
 | ||||||
|  | insert ln17595 << @end | ||||||
|  | 
 | ||||||
|  |     <!-- Ykit --> | ||||||
|  | 
 | ||||||
|  |     <public type="drawable" name="oval_yellow" id="0x7f08f001" /> <!-- YKit-BringBackPhoebe --> | ||||||
|  |     <public type="id" name="ykit_settings" id="0x7f0bf001" /> <!-- YKit-Settings --> | ||||||
|  |     <public type="id" name="ykit_version" id="0x7f0bf003" /> <!-- YKit-Settings --> | ||||||
|  |     <public type="id" name="text_color_yellow" id="0x7f0bf002" /> <!-- YKit-BringBackPhoebe --> | ||||||
|  |     <public type="layout" name="activity_ykit_settings" id="0x7f0ef001" /> <!-- YKit-Settings --> | ||||||
|  |     <public type="raw" name="ykit_meow" id="0x7f12f001" /> <!-- YKit-Settings --> | ||||||
|  | 
 | ||||||
|  | @end
 | ||||||
|  | 
 | ||||||
|  | #17595 | ||||||
							
								
								
									
										6
									
								
								src/patches/yellow.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/patches/yellow.patch
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | file 'res/layout/color_options_toolbar.xml' | ||||||
|  | 
 | ||||||
|  | insert ln8 << @end | ||||||
|  |         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_yellow" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_yellow" /> | ||||||
|  | 
 | ||||||
|  | @end
 | ||||||
							
								
								
									
										6
									
								
								src/patches/ykit_settings.patch
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/patches/ykit_settings.patch
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | file 'res/layout/fragment_blog_settings.xml' | ||||||
|  | 
 | ||||||
|  | insert ln10 << @end | ||||||
|  |             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/ykit_settings" tumblr:listItemDetail="Change settings for YKit" tumblr:listItemTitle="YKit Settings" style="@style/DetailedListItem" /> | ||||||
|  | 
 | ||||||
|  | @end
 | ||||||
|  | @ -1,14 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <merge android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="48.0dip" |  | ||||||
|   xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> |  | ||||||
|     <LinearLayout android:gravity="center" android:orientation="horizontal" android:id="@id/color_editing_controls" android:background="?selectableItemBackgroundBorderless" android:layout_width="fill_parent" android:layout_height="48.0dip"> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_default" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_main_text_color" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_red" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_red" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_orange" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_orange" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_yellow" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_yellow" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_green" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_green" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_blue" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_blue" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_purple" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_purple" /> |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageButton android:id="@id/text_color_pink" android:background="?selectableItemBackgroundBorderless" android:layout_width="48.0dip" android:layout_height="48.0dip" android:layout_marginLeft="2.0dip" android:layout_marginRight="2.0dip" android:layout_weight="0.5" app:srcCompat="@drawable/oval_pink" /> |  | ||||||
|     </LinearLayout> |  | ||||||
| </merge> |  | ||||||
|  | @ -1,43 +0,0 @@ | ||||||
| <?xml version="1.0" encoding="utf-8"?> |  | ||||||
| <com.tumblr.ui.widget.fab.ObservableScrollView android:id="@id/blog_settings_scroll_view" android:background="?themeHighlightedContentBackgroundColor" android:paddingTop="@dimen/action_bar_base_height" android:layout_width="fill_parent" android:layout_height="fill_parent" |  | ||||||
|   xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tumblr="http://schemas.android.com/apk/res-auto"> |  | ||||||
|     <LinearLayout android:background="@null" style="@style/BlogSettingsContainer"> |  | ||||||
|         <LinearLayout android:id="@id/user_blog_default_options" style="@style/BlogSettingsUserBlogOptionsContainer"> |  | ||||||
|             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/account_settings" tumblr:listItemDetail="@string/global_settings_subtitle" tumblr:listItemTitle="@string/account_settings" style="@style/DetailedListItem" /> |  | ||||||
|             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_ad_free_management" android:visibility="gone" tumblr:listItemDetail="@string/tumblr_premium_subtitle" tumblr:listItemTitle="@string/tumblr_premium" style="@style/DetailedListItem" /> |  | ||||||
|             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_payment_and_purchases" tumblr:listItemDetail="@string/payment_and_purchases_desc" tumblr:listItemTitle="@string/payment_and_purchases" style="@style/DetailedListItem" /> |  | ||||||
|             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_subscriptions_and_purchases" tumblr:listItemDetail="@string/subscriptions_and_purchases_desc" tumblr:listItemTitle="@string/subscriptions_and_purchases" style="@style/DetailedListItem" /> |  | ||||||
|             <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/ykit_settings" tumblr:listItemDetail="Change settings for YKit" tumblr:listItemTitle="YKit Settings" style="@style/DetailedListItem" /> |  | ||||||
|         </LinearLayout> |  | ||||||
|         <LinearLayout style="@style/BlogSettingsOptionsSection"> |  | ||||||
|             <TextView android:id="@id/blog_settings_header" android:background="?themeHighlightedContentBackgroundColor" style="@style/SettingsSectionHeader" /> |  | ||||||
|             <com.tumblr.ui.widget.UserBlogOptionsLayout android:id="@id/user_blog_options_container" style="@style/BlogSettingsUserBlogOptionsContainer"> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_change_username_row" android:visibility="gone" tumblr:listItemTitle="@string/change_name_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_pages_row" android:visibility="gone" tumblr:listItemDetail="@string/pages_description" tumblr:listItemTitle="@string/pages" style="@style/DetailedListItem.White" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_featured_tags" android:visibility="gone" tumblr:listItemTitle="@string/featured_tags" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_followers_row" tumblr:listItemTitle="@string/followers_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_drafts_row" tumblr:listItemTitle="@string/drafts_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_queue_row" tumblr:listItemTitle="@string/queue_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_inbox_row" tumblr:listItemDetail="@string/inbox_detail_new" tumblr:listItemTitle="@string/inbox_title" style="@style/DetailedListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_community_label_row" android:visibility="gone" tumblr:listItemTitle="@string/blog_community_label_settings" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_posts_review_row" android:visibility="gone" tumblr:listItemTitle="@string/posts_review_setting_label" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blaze_info_page" android:visibility="gone" tumblr:listItemTitle="@string/tumblr_blaze" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/manage_gifts" android:visibility="gone" tumblr:listItemTitle="@string/gifts" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_payouts" android:visibility="gone" tumblr:listItemTitle="@string/payouts_settings" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_privacy_row" tumblr:listItemTitle="@string/setting_label_blog_privacy_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_delete_row" tumblr:listItemTitle="@string/delete_blog" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleWithWarningRow android:id="@id/blog_allow_tipping" android:paddingStart="6.0dip" tumblr:rowText="@string/allow_tipping" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleWithWarningRow android:id="@id/blog_allow_blaze_by_others" android:paddingStart="6.0dip" tumblr:rowText="@string/allow_blaze_by_others_v2" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_show_tip_button" android:paddingStart="6.0dip" tumblr:rowText="@string/tipping_tip_blog" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_post_tipping_default" android:paddingStart="6.0dip" tumblr:rowText="@string/tipping_post_default" tumblr:toggleDefault="false" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_allow_submissions" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_allow_submissions" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_allow_asks" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_allow_asks" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_ask_allow_anonymous" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_ask_allow_anonymous" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_ask_allow_media" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_ask_allow_with_media" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_ask_page_title" android:minHeight="0.0dip" tumblr:listItemTitle="@string/blog_ask_page_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMToggleRow android:id="@id/blog_show_top_posts" android:paddingStart="6.0dip" tumblr:rowText="@string/blog_show_top_posts" tumblr:toggleDefault="true" style="@style/SettingsToggleSettingRow" /> |  | ||||||
|                 <com.tumblr.ui.widget.TMBlogSettingsTextRow android:id="@id/blog_blocked_tumblrs_row" tumblr:listItemTitle="@string/blocked_tumblrs_title" style="@style/BlogSettingsListItem" /> |  | ||||||
|             </com.tumblr.ui.widget.UserBlogOptionsLayout> |  | ||||||
|         </LinearLayout> |  | ||||||
|     </LinearLayout> |  | ||||||
| </com.tumblr.ui.widget.fab.ObservableScrollView> |  | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										3
									
								
								test.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | from buildtool.polly.patcher import process_patch_file | ||||||
|  | 
 | ||||||
|  | process_patch_file('src/patches/public.patch', check_date=True) | ||||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								tools/d8.jar
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tools/d8.jar
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	Add table
		
		Reference in a new issue