From 8eeb22c0496734aa61244bb264e849caa38b318a Mon Sep 17 00:00:00 2001 From: Sobottasgithub Date: Thu, 25 Apr 2024 17:06:57 +0200 Subject: [PATCH] fix: Release an apk for the mobile log viewer as well, fixes #19 --- .github/workflows/autorelease.yaml | 85 +++++++++++++++++- src/MobileLogViewer/data/conf/buildozer.spec | 30 +++++++ .../data/conf/intent_filters.xml | 61 +++++++++++++ .../data/conf/release.keystore.enc | Bin 0 -> 4426 bytes src/MobileLogViewer/ui/main_window.py | 79 ++++++++++++++++ 5 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 src/MobileLogViewer/data/conf/buildozer.spec create mode 100644 src/MobileLogViewer/data/conf/intent_filters.xml create mode 100644 src/MobileLogViewer/data/conf/release.keystore.enc diff --git a/.github/workflows/autorelease.yaml b/.github/workflows/autorelease.yaml index 4224bfd..9f06fb1 100644 --- a/.github/workflows/autorelease.yaml +++ b/.github/workflows/autorelease.yaml @@ -10,7 +10,7 @@ on: jobs: # see https://data-dive.com/multi-os-deployment-in-cloud-using-pyinstaller-and-github-actions/ # but here we build first and run our createrelease job afterwards, accessing the artifacts of our build job - build-android: + build-android-MMCA: name: Build MobileCrashAnalyzer for android runs-on: ubuntu-latest @@ -21,7 +21,8 @@ jobs: - name: Install dependencies run: sudo apt -y install apksigner - - name: Prepare app for buildozer + # Build Mobile Crash Analyzer + - name: Prepare MMCA for buildozer run: | REFNAME="${{ github.ref_name }}" echo "VERSION='${REFNAME}'" >src/shared/utils/version.py @@ -85,6 +86,83 @@ jobs: with: name: MobileCrashAnalyzer path: ${{ steps.signer.outputs.signed_apk }} + + build-android-MMLV: + name: Build MobileLogViewer for android + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt -y install apksigner + + # Build Monal Mobile Log Viewer + - name: Prepare MMLV for buildozer + run: | + REFNAME="${{ github.ref_name }}" + echo "VERSION='${REFNAME}'" >src/shared/utils/version.py + + mkdir MobileLogViewer_Deploy + cp -av src/MobileLogViewer.py MobileLogViewer_Deploy/ + cp -av src/MobileLogViewer MobileLogViewer_Deploy/ + cp -av src/shared MobileLogViewer_Deploy/ + + cp -av src/MobileLogViewer/data/conf/intent_filters.xml MobileLogViewer_Deploy/ + cp -av src/MobileLogViewer/data/conf/buildozer.spec MobileLogViewer_Deploy/ + echo "" >>MobileLogViewer_Deploy/buildozer.spec + # buildozer does not like non-numeric version numbers + echo "version = ${REFNAME#v}" >>MobileLogViewer_Deploy/buildozer.spec + + # dynamically build main.py proxy to load + echo "import os" >MobileLogViewer_Deploy/main.py + echo "os.environ['KIVY_LOG_MODE'] = 'MIXED'" >>MobileLogViewer_Deploy/main.py + echo "os.environ['KIVY_NO_FILELOG'] = '1'" >>MobileLogViewer_Deploy/main.py + echo "from importlib import util" >>MobileLogViewer_Deploy/main.py + echo "real_file = os.path.join(os.path.dirname(__file__), 'MobileLogViewer.pyc')" >>MobileLogViewer_Deploy/main.py + echo "print('Proxy loading real file: %s' % real_file)" >>MobileLogViewer_Deploy/main.py + echo "" >>MobileLogViewer_Deploy/main.py + echo "spec = util.spec_from_file_location('MobileLogViewer', real_file)" >>MobileLogViewer_Deploy/main.py + echo "print(f'{spec = }')" >>MobileLogViewer_Deploy/main.py + echo "mod = util.module_from_spec(spec)" >>MobileLogViewer_Deploy/main.py + echo "# we don't want to add our module to sys.modules to not interfere with the package path having the same name" >>MobileLogViewer_Deploy/main.py + echo "#(just like if our real_file was the real entry point)" >>MobileLogViewer_Deploy/main.py + echo "print(f'{mod = }')" >>MobileLogViewer_Deploy/main.py + echo "out = spec.loader.exec_module(mod)" >>MobileLogViewer_Deploy/main.py + echo "print(f'{out = }')" >>MobileLogViewer_Deploy/main.py + echo "print('Proxied file returned, terminating...')" >>MobileLogViewer_Deploy/main.py + + echo "" + echo "buildozer.spec:" + cat MobileLogViewer_Deploy/buildozer.spec + echo "" + echo "main.py:" + cat MobileLogViewer_Deploy/main.py + + - name: Build with Buildozer + uses: Sobottasgithub/buildozer-action@v1.1.3 + id: buildozer + with: + workdir: MobileLogViewer_Deploy + buildozer_version: stable + + - name: Sign Apk + id: signer + run: | + apkinfile="${{ steps.buildozer.outputs.filename }}" + apkoutfile="$HOME/signed-$(basename "${apkinfile}")" + echo -n "${{ secrets.APK_SIGNING_KEY }}" >"$HOME/keystore.key" + openssl enc -d -chacha20 -pbkdf2 -in src/MobileLogViewer/data/conf/release.keystore.enc -out "$HOME/release.keystore" -kfile "$HOME/keystore.key" + cat "$HOME/keystore.key" | apksigner sign -ks "$HOME/release.keystore" -in "${apkinfile}" -out "${apkoutfile}" + echo "unsigned_apk=${apkinfile}" >> "$GITHUB_OUTPUT" + echo "signed_apk=${apkoutfile}" >> "$GITHUB_OUTPUT" + + - name: Upload signed apk + uses: actions/upload-artifact@v4 + with: + name: MobileLogViewer + path: ${{ steps.signer.outputs.signed_apk }} build-desktop: name: Build ${{ matrix.OUT_FILE_NAME }} for ${{ matrix.TARGET }} @@ -184,7 +262,7 @@ jobs: createrelease: name: Create Release runs-on: [ubuntu-latest] - needs: [build-desktop, build-android] + needs: [build-desktop, build-android-MMCA, build-android-MMLV] steps: - name: Load build artifacts uses: actions/download-artifact@v4 @@ -209,6 +287,7 @@ jobs: LogViewer*/* CrashAnalyzer*/* MobileCrashAnalyzer*/* + MobileLogViewer*/* fail_on_unmatched_files: true token: ${{ secrets.GITHUB_TOKEN }} draft: false diff --git a/src/MobileLogViewer/data/conf/buildozer.spec b/src/MobileLogViewer/data/conf/buildozer.spec new file mode 100644 index 0000000..6799b8a --- /dev/null +++ b/src/MobileLogViewer/data/conf/buildozer.spec @@ -0,0 +1,30 @@ +[buildozer] +log_level = 2 + +[app] +title = Mobile Log Viewer +package.name = MobileLogViewer +package.domain = im.monal + +source.dir = . +source.include_exts = py,png,jpg,kv,atlas,json +source.include_patterns = shared/*,MobileLogViewer/* +icon.filename = %(source.dir)s/MobileLogViewer/data/art/icon.png +presplash.filename = %(source.dir)s/MobileLogViewer/data/art/icon.png + +requirements = python3,kivy,platformdirs,logging,jnius + +orientation = portrait +fullscreen = 0 + +# android specific +android.minapi = 29 +android.permissions = WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE +android.arch = armeabi-v7a +android.manifest.intent_filters = intent_filters.xml + +# iOS specific +ios.kivy_ios_url = https://github.com/kivy/kivy-ios +ios.kivy_ios_branch = master +ios.ios_deploy_url = https://github.com/phonegap/ios-deploy +ios.ios_deploy_branch = 1.7.0 diff --git a/src/MobileLogViewer/data/conf/intent_filters.xml b/src/MobileLogViewer/data/conf/intent_filters.xml new file mode 100644 index 0000000..06576d1 --- /dev/null +++ b/src/MobileLogViewer/data/conf/intent_filters.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MobileLogViewer/data/conf/release.keystore.enc b/src/MobileLogViewer/data/conf/release.keystore.enc new file mode 100644 index 0000000000000000000000000000000000000000..7110cc0f0abd2e4cb3101700c987fde8a60f354b GIT binary patch literal 4426 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 QWnhM+SeW6fe;=N?G41HIH2?qr literal 0 HcmV?d00001 diff --git a/src/MobileLogViewer/ui/main_window.py b/src/MobileLogViewer/ui/main_window.py index a85b421..6b1fcc4 100644 --- a/src/MobileLogViewer/ui/main_window.py +++ b/src/MobileLogViewer/ui/main_window.py @@ -3,14 +3,27 @@ from kivy.uix.filechooser import FileChooserListView from kivy.uix.scrollview import ScrollView +from kivy.clock import mainthread from kivy.uix.gridlayout import GridLayout from kivy.uix.button import Button from kivy.uix.label import Label from kivy.uix.popup import Popup from kivy.factory import Factory +from kivy.utils import platform import functools +import os from collections import defaultdict +from jnius import autoclass, cast + +# Just import if the os is Android to avoid Android peculiarities +if platform == "android": + from android import activity, mActivity, permissions + J_FileOutputStream = autoclass("java.io.FileOutputStream") + J_FileUtils = autoclass("android.os.FileUtils") + #J_Intent = autoclass("android.content.Intent") + #J_PythonActivity = autoclass('org.kivy.android.PythonActivity') + permissions.request_permissions([permissions.Permission.READ_EXTERNAL_STORAGE, permissions.Permission.WRITE_EXTERNAL_STORAGE]) from shared.utils.constants import LOGLEVELS from shared.storage import Rawlog @@ -72,10 +85,76 @@ def build(self): self.layout.add_widget(gridLayout_menueBar) self.layout.add_widget(self.uiScrollWidget_logs) + # Just import if the os is Android to avoid Android peculiarities + if platform == "android": + activity.bind(on_new_intent=self.on_new_intent) + return self.layout def quit(self, *args): self.stop() + + def on_start(self, *args): + # Just import if the os is Android to avoid Android peculiarities + if platform == "android": + context = cast('android.content.Context', mActivity.getApplicationContext()) + logger.info(f"Startup application context: {context}") + intent = mActivity.getIntent() + logger.info(f"Got startup intent: {intent}") + if intent: + self.on_new_intent(intent) + + #see https://github.com/termux/termux-app/blob/74b23cb2096652601050d0f4951f9fb92577743c/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java#L70 + @mainthread + def on_new_intent(self, intent): + logger.info("Got new intent with action: %s" % str(intent.getAction())) + logger.debug("Raw intent: %s" % str(intent)) + if intent.getAction() == "android.intent.action.VIEW": + logger.info("Intent scheme: %s" % intent.getScheme()) + logger.info("Intent type: %s" % intent.getType()) + logger.info("Intent data: %s" % intent.getData()) + logger.info("Intent path: %s" % intent.getData().getPath()) + + uri = intent.getData() + context = mActivity.getApplicationContext() + contentResolver = context.getContentResolver() + + cacheFile = Paths.get_cache_filepath("intent.file") + if os.path.exists(cacheFile): + os.remove(cacheFile) + logger.debug(f"Writing file at '{uri.getPath()}' to '{cacheFile}'...") + bytecount = J_FileUtils.copy(contentResolver.openInputStream(uri), J_FileOutputStream(cacheFile)) + logger.debug(f"{bytecount} bytes copied...") + self.openFile(cacheFile) + os.remove(cacheFile) + + """ + logger.info("Intent uri: %s" % intent.getParcelableExtra(J_Intent.EXTRA_STREAM)) + logger.info("Intent text: %s" % intent.getStringExtra(J_Intent.EXTRA_TEXT)) + uri = intent.getParcelableExtra(J_Intent.EXTRA_STREAM) + context = mActivity.getApplicationContext() + contentResolver = context.getContentResolver() + if uri != None and type(uri) != str: + logger.info("Real android.net.Uri found...") + uri = cast("android.net.Uri", uri) + if uri.getScheme().lower() != 'content': + logger.error("Uri scheme not supported: '%s'" % uri.getScheme()) + return + cacheFile = Paths.get_cache_filepath("intent.file") + if os.path.exists(cacheFile): + os.remove(cacheFile) + J_FileUtils.copy(contentResolver.openInputStream(uri), J_FileOutputStream(cacheFile)) + self.openFile(cacheFile) + os.remove(cacheFile) + else: + logger.info("Str based uri found...") + cacheFile = Paths.get_cache_filepath("intent.file") + if os.path.exists(cacheFile): + os.remove(cacheFile) + J_FileUtils.copy(contentResolver.openInputStream(intent.getData()), J_FileOutputStream(cacheFile)) + self.openFile(cacheFile) + os.remove(cacheFile) + """ def selectFile(self, *args): # Create popup window to select file