📲Android端末上でAndroidアプリをビルドする2024/12/13に公開2025/02/254件AndroidVS CodeUbuntubuildTermuxtechDiscussionmojabi2025/03/15に更新Androidのアプリ開発はあまり詳しくないですが、非常に参考になりました。 aapt2の差し替えは、zip/unzipではうまくいかず、jar cf/jar xfで圧縮/解凍を行うと、期待通りの動作をしました。 私の環境(Pixel9 Fold)では、gradle.propertiesで-Xmx8g -Xms8gと設定しないと、aapt2が動作しませんでした。 急いで作ったのでかなりベタベタですが、以下のような適当なスクリプトで最小限のプロジェクトを作って、いろいろいじって遊んでいます。 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import importlib.util import textwrap import sys # ----- Utilities ----- def check_packages(packages): not_installed = [] for package in packages: if importlib.util.find_spec(package) is None: not_installed.append(package) return not_installed def bail(message, status): print(message) sys.exit(status) def help(): help = ''' android_create_project.py <target_sdk> <min_sdk> <package_name> <project_name> <output_dir> ''' bail(textwrap.dedent(help).strip(), 1) # ----- main ----- def main(target_sdk, min_sdk, package_name, project_name, output_dir): # プロジェクトのディレクトリを作成 project_path = os.path.join(output_dir, project_name) os.makedirs(project_path, exist_ok=True) # 各パスの作成 app_path = os.path.join(project_path, 'app') main_path = os.path.join(app_path, 'src/main') java_path = os.path.join(main_path, 'java') package_path = os.path.join(java_path, package_name.replace('.', '/')) res_path = os.path.join(main_path, 'res') mipmap_path = os.path.join(res_path, 'mipmap-xxxhdpi') layout_path = os.path.join(res_path, 'layout') values_path = os.path.join(res_path, 'values') # ディレクトリの作成 os.makedirs(os.path.join(package_path), exist_ok=True) os.makedirs(os.path.join(mipmap_path), exist_ok=True) os.makedirs(os.path.join(layout_path), exist_ok=True) os.makedirs(os.path.join(values_path), exist_ok=True) # 各ファイルの出力 ### gradle.properties gradle_properties = f"""org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx8g -Xms8g android.useAndroidX=true """ ### build.gradle (ルート) root_build_gradle = f"""buildscript {{ repositories {{ google() mavenCentral() }} dependencies {{ classpath "com.android.tools.build:gradle:8.5.1" }} }} allprojects {{ repositories {{ google() mavenCentral() }} }} """ # app/build.gradle app_build_gradle = f"""plugins {{ id 'com.android.application' }} android {{ namespace '{package_name}' compileSdk {target_sdk} defaultConfig {{ applicationId "{package_name}" minSdk {min_sdk} targetSdk {target_sdk} versionCode 1 versionName "1.0" }} buildTypes {{ release {{ minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' }} }} }} dependencies {{ implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' }} """ # AndroidManifest.xml android_manifest = f"""<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{package_name}"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.{project_name}"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> """ ### MainActivity.java main_activity = f"""package {package_name}; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity {{ @Override protected void onCreate(Bundle savedInstanceState) {{ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }} }} """ ### activity_main.xml activity_main_xml = f"""<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> """ ### strings.xml strings_xml = f"""<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">{project_name}</string> </resources> """ ### themes.xml themes_xml = f"""<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"> <style name="Theme.{project_name}" parent="Theme.Design.Light.NoActionBar"> </style> </resources> """ # ファイル出力 with open(os.path.join(project_path, 'gradle.properties'), 'w') as f: f.write(gradle_properties) with open(os.path.join(project_path, 'build.gradle'), 'w') as f: f.write(root_build_gradle) with open(os.path.join(app_path, 'build.gradle'), 'w') as f: f.write(app_build_gradle) with open(os.path.join(main_path, 'AndroidManifest.xml'), 'w') as f: f.write(android_manifest) with open(os.path.join(package_path, 'MainActivity.java'), 'w') as f: f.write(main_activity) with open(os.path.join(layout_path, 'activity_main.xml'), 'w') as f: f.write(activity_main_xml) with open(os.path.join(values_path, 'strings.xml'), 'w') as f: f.write(strings_xml) with open(os.path.join(values_path, 'themes.xml'), 'w') as f: f.write(themes_xml) # アイコン作成 width, height = 192, 192 color = (255, 0, 0) # RGB値で赤色 image = Image.new('RGB', (width, height), color) image.save(os.path.join(mipmap_path, 'ic_launcher.png')) image.save(os.path.join(mipmap_path, 'ic_launcher_round.png')) # TODO # Gradleラッパーを生成 subprocess.run(['gradle', 'wrapper', '--gradle-version', '8.12'], cwd=project_path, check=True) ### settings.gradle settings_gradle = f"""rootProject.name = '{project_name}' include ':app' """ with open(os.path.join(project_path, 'settings.gradle'), 'w') as f: f.write(settings_gradle) # ----- Entry point ----- if __name__ == "__main__": required_packages = ['PIL'] not_installed = check_packages(required_packages) if not_installed: print("本スクリプトの実行には、以下のパッケージをインストールする必要があります。\n") for package in not_installed: print(f" - {package}\n") sys.exit(1) else: import os import subprocess import sys from PIL import Image args = sys.argv if len(args) >= 2 and (args[1] == '-h' or args[1] == '--help'): help() if len(args) == 6: target_sdk = args[1] min_sdk = args[2] package_name = args[3] project_name = args[4] output_dir = args[5] main(target_sdk, min_sdk, package_name, project_name, output_dir) sys.exit(0) help() turtlekazu🐢2025/03/25コメントありがとうございます! zip/unzipではうまくいかない場合があるとは思い至りませんでした。ご共有くださいまして、ありがとうございます。 -Xmx8g -Xms8gはかなり大きな値ですね。。。Pixel Foldだと、Android端末上の開発も快適にできそうです。 Androidプロジェクト自体をPythonから生成してしまうの、すごすぎます👀 mojabi2025/03/26返信ありがとうございます。 本当に貴重な記事をありがとうございます。 コンパイルからインストールまで、なんとかコマンドライン一発でできないかと思い、以下のようなアプリを作成しました。 import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import java.io.File; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private String apkPath; private LinearLayout layout; @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (intent == null) { return; } apkPath = intent.getStringExtra("apkPath"); if (apkPath != null) { installApk(apkPath); } else { Toast .makeText(this, getString(R.string.message_no_arguement_apk_path), Toast.LENGTH_SHORT) .show(); } finish(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); layout = new LinearLayout(this); layout.setOrientation(LinearLayout.VERTICAL); setContentView(layout); if (checkStoragePermission()) { onNewIntent(getIntent()); return; } Button button = new Button(this); button.setText(getString(R.string.title_request_permission)); button.setOnClickListener(v -> { requestAllFilesAccess(); }); layout.addView(button); } public boolean checkStoragePermission() { return (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || Environment.isExternalStorageManager(); } public void requestAllFilesAccess() { if (Environment.isExternalStorageManager()) { return; } requestAllFilesAccessPermission(); } private void requestAllFilesAccessPermission() { try { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } catch (ActivityNotFoundException anf) { try { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:$packageName")); startActivity(intent); } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_get_permission), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast.makeText(this, getString(R.string.message_failed_to_initialize_activity_to_request_all_files_access), Toast.LENGTH_SHORT) .show(); } } private void installApk(String apkPath) { try { File file = new File(apkPath); if (file.exists()) { Uri uri = FileProvider.getUriForFile (this, getApplicationContext().getPackageName() + ".provider", file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, "application/vnd.android.package-archive"); intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); Toast .makeText(this, getString(R.string.message_start_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } else { Toast .makeText(this, getString(R.string.message_no_apk_file), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } } } 文字列リソースは以下のような感じです。 <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">インストーラー</string> <string name="title_request_permission">権限をリクエスト</string> <string name="message_no_arguement_apk_path">APKファイルパスを指定してください。</string> <string name="message_failed_to_get_permission">必要な権限の取得に失敗しました。</string> <string name="message_failed_to_initialize_activity_to_request_all_files_access">権限取得用画面の初期化に失敗しました。</string> <string name="message_start_to_install_apk_file">インストールを開始しました。</string> <string name="message_no_apk_file">正しいAPKファイルパスを指定してください。</string> <string name="message_failed_to_install_apk_file">インストール中にエラーが発生しました。</string> </resources> 上記を、例えばパッケージ名 com.example.app.installer に設定してコンパイルし、ファイルマネージャーなどからインストールします。 起動後に画面上のボタンを押して「すべてのファイルへのアクセス」権限をオンにした状態で、Ubuntu on Termux から以下のコマンドを実行すると、Androidのインストーラー起動まではコマンド一発でいけます。 予め、 /storage/emulated/0/Termux ディレクトリを作成しておいてください。 例えば MyApp というプロジェクト内で実行すると、こんなかんじです。 app=MyApp && ./gradlew assembleDebug && cp app/build/outputs/apk/debug/app-debug.apk ~/Android/${app}.apk && am start -n com.examle.app.installer/.MainActivity --es apkPath /storage/emulated/0/Termux/${app}.apk セキュリテイの制約で、何回か画面をタップしないとコンパイルしたアプリをインストールできませんが、無いよりかましかと。 これで気になるコードを、Android端末だけでたくさん試すことができます。 私はEmacsユーザーなので、上記コマンドをEmacs上から生成して実行できるLispを作成して C-r キーで実行しています。 記事中のVSCodeは使ったことがないのですが、たしかVSCodeでもターミナル機能があったと思うので、同じように実行できるかと。 mojabi2025/03/26すみません。マニフェストと <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app.installer"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Design.Light.NoActionBar"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> </application> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> </manifest> providerです。 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path path="Android/data/${applicationId}/" name="package_root" /> <cache-path path="Android/data/${applicationId}/" name="cache" /> <root-path path="." name="storage_root" /> </paths> これがないと権限関連が混乱する話になるところでした。。。 返信を追加
mojabi2025/03/15に更新Androidのアプリ開発はあまり詳しくないですが、非常に参考になりました。 aapt2の差し替えは、zip/unzipではうまくいかず、jar cf/jar xfで圧縮/解凍を行うと、期待通りの動作をしました。 私の環境(Pixel9 Fold)では、gradle.propertiesで-Xmx8g -Xms8gと設定しないと、aapt2が動作しませんでした。 急いで作ったのでかなりベタベタですが、以下のような適当なスクリプトで最小限のプロジェクトを作って、いろいろいじって遊んでいます。 #!/usr/bin/env python3 # -*- coding: utf-8 -*- import importlib.util import textwrap import sys # ----- Utilities ----- def check_packages(packages): not_installed = [] for package in packages: if importlib.util.find_spec(package) is None: not_installed.append(package) return not_installed def bail(message, status): print(message) sys.exit(status) def help(): help = ''' android_create_project.py <target_sdk> <min_sdk> <package_name> <project_name> <output_dir> ''' bail(textwrap.dedent(help).strip(), 1) # ----- main ----- def main(target_sdk, min_sdk, package_name, project_name, output_dir): # プロジェクトのディレクトリを作成 project_path = os.path.join(output_dir, project_name) os.makedirs(project_path, exist_ok=True) # 各パスの作成 app_path = os.path.join(project_path, 'app') main_path = os.path.join(app_path, 'src/main') java_path = os.path.join(main_path, 'java') package_path = os.path.join(java_path, package_name.replace('.', '/')) res_path = os.path.join(main_path, 'res') mipmap_path = os.path.join(res_path, 'mipmap-xxxhdpi') layout_path = os.path.join(res_path, 'layout') values_path = os.path.join(res_path, 'values') # ディレクトリの作成 os.makedirs(os.path.join(package_path), exist_ok=True) os.makedirs(os.path.join(mipmap_path), exist_ok=True) os.makedirs(os.path.join(layout_path), exist_ok=True) os.makedirs(os.path.join(values_path), exist_ok=True) # 各ファイルの出力 ### gradle.properties gradle_properties = f"""org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=1 -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx8g -Xms8g android.useAndroidX=true """ ### build.gradle (ルート) root_build_gradle = f"""buildscript {{ repositories {{ google() mavenCentral() }} dependencies {{ classpath "com.android.tools.build:gradle:8.5.1" }} }} allprojects {{ repositories {{ google() mavenCentral() }} }} """ # app/build.gradle app_build_gradle = f"""plugins {{ id 'com.android.application' }} android {{ namespace '{package_name}' compileSdk {target_sdk} defaultConfig {{ applicationId "{package_name}" minSdk {min_sdk} targetSdk {target_sdk} versionCode 1 versionName "1.0" }} buildTypes {{ release {{ minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' }} }} }} dependencies {{ implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' }} """ # AndroidManifest.xml android_manifest = f"""<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{package_name}"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.{project_name}"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> """ ### MainActivity.java main_activity = f"""package {package_name}; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity {{ @Override protected void onCreate(Bundle savedInstanceState) {{ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }} }} """ ### activity_main.xml activity_main_xml = f"""<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> </FrameLayout> """ ### strings.xml strings_xml = f"""<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">{project_name}</string> </resources> """ ### themes.xml themes_xml = f"""<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools"> <style name="Theme.{project_name}" parent="Theme.Design.Light.NoActionBar"> </style> </resources> """ # ファイル出力 with open(os.path.join(project_path, 'gradle.properties'), 'w') as f: f.write(gradle_properties) with open(os.path.join(project_path, 'build.gradle'), 'w') as f: f.write(root_build_gradle) with open(os.path.join(app_path, 'build.gradle'), 'w') as f: f.write(app_build_gradle) with open(os.path.join(main_path, 'AndroidManifest.xml'), 'w') as f: f.write(android_manifest) with open(os.path.join(package_path, 'MainActivity.java'), 'w') as f: f.write(main_activity) with open(os.path.join(layout_path, 'activity_main.xml'), 'w') as f: f.write(activity_main_xml) with open(os.path.join(values_path, 'strings.xml'), 'w') as f: f.write(strings_xml) with open(os.path.join(values_path, 'themes.xml'), 'w') as f: f.write(themes_xml) # アイコン作成 width, height = 192, 192 color = (255, 0, 0) # RGB値で赤色 image = Image.new('RGB', (width, height), color) image.save(os.path.join(mipmap_path, 'ic_launcher.png')) image.save(os.path.join(mipmap_path, 'ic_launcher_round.png')) # TODO # Gradleラッパーを生成 subprocess.run(['gradle', 'wrapper', '--gradle-version', '8.12'], cwd=project_path, check=True) ### settings.gradle settings_gradle = f"""rootProject.name = '{project_name}' include ':app' """ with open(os.path.join(project_path, 'settings.gradle'), 'w') as f: f.write(settings_gradle) # ----- Entry point ----- if __name__ == "__main__": required_packages = ['PIL'] not_installed = check_packages(required_packages) if not_installed: print("本スクリプトの実行には、以下のパッケージをインストールする必要があります。\n") for package in not_installed: print(f" - {package}\n") sys.exit(1) else: import os import subprocess import sys from PIL import Image args = sys.argv if len(args) >= 2 and (args[1] == '-h' or args[1] == '--help'): help() if len(args) == 6: target_sdk = args[1] min_sdk = args[2] package_name = args[3] project_name = args[4] output_dir = args[5] main(target_sdk, min_sdk, package_name, project_name, output_dir) sys.exit(0) help() turtlekazu🐢2025/03/25コメントありがとうございます! zip/unzipではうまくいかない場合があるとは思い至りませんでした。ご共有くださいまして、ありがとうございます。 -Xmx8g -Xms8gはかなり大きな値ですね。。。Pixel Foldだと、Android端末上の開発も快適にできそうです。 Androidプロジェクト自体をPythonから生成してしまうの、すごすぎます👀 mojabi2025/03/26返信ありがとうございます。 本当に貴重な記事をありがとうございます。 コンパイルからインストールまで、なんとかコマンドライン一発でできないかと思い、以下のようなアプリを作成しました。 import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import java.io.File; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private String apkPath; private LinearLayout layout; @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (intent == null) { return; } apkPath = intent.getStringExtra("apkPath"); if (apkPath != null) { installApk(apkPath); } else { Toast .makeText(this, getString(R.string.message_no_arguement_apk_path), Toast.LENGTH_SHORT) .show(); } finish(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); layout = new LinearLayout(this); layout.setOrientation(LinearLayout.VERTICAL); setContentView(layout); if (checkStoragePermission()) { onNewIntent(getIntent()); return; } Button button = new Button(this); button.setText(getString(R.string.title_request_permission)); button.setOnClickListener(v -> { requestAllFilesAccess(); }); layout.addView(button); } public boolean checkStoragePermission() { return (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || Environment.isExternalStorageManager(); } public void requestAllFilesAccess() { if (Environment.isExternalStorageManager()) { return; } requestAllFilesAccessPermission(); } private void requestAllFilesAccessPermission() { try { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } catch (ActivityNotFoundException anf) { try { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:$packageName")); startActivity(intent); } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_get_permission), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast.makeText(this, getString(R.string.message_failed_to_initialize_activity_to_request_all_files_access), Toast.LENGTH_SHORT) .show(); } } private void installApk(String apkPath) { try { File file = new File(apkPath); if (file.exists()) { Uri uri = FileProvider.getUriForFile (this, getApplicationContext().getPackageName() + ".provider", file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, "application/vnd.android.package-archive"); intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); Toast .makeText(this, getString(R.string.message_start_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } else { Toast .makeText(this, getString(R.string.message_no_apk_file), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } } } 文字列リソースは以下のような感じです。 <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">インストーラー</string> <string name="title_request_permission">権限をリクエスト</string> <string name="message_no_arguement_apk_path">APKファイルパスを指定してください。</string> <string name="message_failed_to_get_permission">必要な権限の取得に失敗しました。</string> <string name="message_failed_to_initialize_activity_to_request_all_files_access">権限取得用画面の初期化に失敗しました。</string> <string name="message_start_to_install_apk_file">インストールを開始しました。</string> <string name="message_no_apk_file">正しいAPKファイルパスを指定してください。</string> <string name="message_failed_to_install_apk_file">インストール中にエラーが発生しました。</string> </resources> 上記を、例えばパッケージ名 com.example.app.installer に設定してコンパイルし、ファイルマネージャーなどからインストールします。 起動後に画面上のボタンを押して「すべてのファイルへのアクセス」権限をオンにした状態で、Ubuntu on Termux から以下のコマンドを実行すると、Androidのインストーラー起動まではコマンド一発でいけます。 予め、 /storage/emulated/0/Termux ディレクトリを作成しておいてください。 例えば MyApp というプロジェクト内で実行すると、こんなかんじです。 app=MyApp && ./gradlew assembleDebug && cp app/build/outputs/apk/debug/app-debug.apk ~/Android/${app}.apk && am start -n com.examle.app.installer/.MainActivity --es apkPath /storage/emulated/0/Termux/${app}.apk セキュリテイの制約で、何回か画面をタップしないとコンパイルしたアプリをインストールできませんが、無いよりかましかと。 これで気になるコードを、Android端末だけでたくさん試すことができます。 私はEmacsユーザーなので、上記コマンドをEmacs上から生成して実行できるLispを作成して C-r キーで実行しています。 記事中のVSCodeは使ったことがないのですが、たしかVSCodeでもターミナル機能があったと思うので、同じように実行できるかと。 mojabi2025/03/26すみません。マニフェストと <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app.installer"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Design.Light.NoActionBar"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> </application> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> </manifest> providerです。 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path path="Android/data/${applicationId}/" name="package_root" /> <cache-path path="Android/data/${applicationId}/" name="cache" /> <root-path path="." name="storage_root" /> </paths> これがないと権限関連が混乱する話になるところでした。。。 返信を追加
turtlekazu🐢2025/03/25コメントありがとうございます! zip/unzipではうまくいかない場合があるとは思い至りませんでした。ご共有くださいまして、ありがとうございます。 -Xmx8g -Xms8gはかなり大きな値ですね。。。Pixel Foldだと、Android端末上の開発も快適にできそうです。 Androidプロジェクト自体をPythonから生成してしまうの、すごすぎます👀
mojabi2025/03/26返信ありがとうございます。 本当に貴重な記事をありがとうございます。 コンパイルからインストールまで、なんとかコマンドライン一発でできないかと思い、以下のようなアプリを作成しました。 import android.Manifest; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; import java.io.File; public class MainActivity extends AppCompatActivity { private static final String TAG = MainActivity.class.getSimpleName(); private String apkPath; private LinearLayout layout; @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (intent == null) { return; } apkPath = intent.getStringExtra("apkPath"); if (apkPath != null) { installApk(apkPath); } else { Toast .makeText(this, getString(R.string.message_no_arguement_apk_path), Toast.LENGTH_SHORT) .show(); } finish(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); layout = new LinearLayout(this); layout.setOrientation(LinearLayout.VERTICAL); setContentView(layout); if (checkStoragePermission()) { onNewIntent(getIntent()); return; } Button button = new Button(this); button.setText(getString(R.string.title_request_permission)); button.setOnClickListener(v -> { requestAllFilesAccess(); }); layout.addView(button); } public boolean checkStoragePermission() { return (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || (ActivityCompat.checkSelfPermission (this, Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) == PackageManager.PERMISSION_GRANTED) || Environment.isExternalStorageManager(); } public void requestAllFilesAccess() { if (Environment.isExternalStorageManager()) { return; } requestAllFilesAccessPermission(); } private void requestAllFilesAccessPermission() { try { Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:" + getPackageName())); startActivity(intent); } catch (ActivityNotFoundException anf) { try { Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) .setData(Uri.parse("package:$packageName")); startActivity(intent); } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_get_permission), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast.makeText(this, getString(R.string.message_failed_to_initialize_activity_to_request_all_files_access), Toast.LENGTH_SHORT) .show(); } } private void installApk(String apkPath) { try { File file = new File(apkPath); if (file.exists()) { Uri uri = FileProvider.getUriForFile (this, getApplicationContext().getPackageName() + ".provider", file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(uri, "application/vnd.android.package-archive"); intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION); startActivity(intent); Toast .makeText(this, getString(R.string.message_start_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } else { Toast .makeText(this, getString(R.string.message_no_apk_file), Toast.LENGTH_SHORT) .show(); } } catch (Exception e) { Toast .makeText(this, getString(R.string.message_failed_to_install_apk_file), Toast.LENGTH_SHORT) .show(); } } } 文字列リソースは以下のような感じです。 <?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">インストーラー</string> <string name="title_request_permission">権限をリクエスト</string> <string name="message_no_arguement_apk_path">APKファイルパスを指定してください。</string> <string name="message_failed_to_get_permission">必要な権限の取得に失敗しました。</string> <string name="message_failed_to_initialize_activity_to_request_all_files_access">権限取得用画面の初期化に失敗しました。</string> <string name="message_start_to_install_apk_file">インストールを開始しました。</string> <string name="message_no_apk_file">正しいAPKファイルパスを指定してください。</string> <string name="message_failed_to_install_apk_file">インストール中にエラーが発生しました。</string> </resources> 上記を、例えばパッケージ名 com.example.app.installer に設定してコンパイルし、ファイルマネージャーなどからインストールします。 起動後に画面上のボタンを押して「すべてのファイルへのアクセス」権限をオンにした状態で、Ubuntu on Termux から以下のコマンドを実行すると、Androidのインストーラー起動まではコマンド一発でいけます。 予め、 /storage/emulated/0/Termux ディレクトリを作成しておいてください。 例えば MyApp というプロジェクト内で実行すると、こんなかんじです。 app=MyApp && ./gradlew assembleDebug && cp app/build/outputs/apk/debug/app-debug.apk ~/Android/${app}.apk && am start -n com.examle.app.installer/.MainActivity --es apkPath /storage/emulated/0/Termux/${app}.apk セキュリテイの制約で、何回か画面をタップしないとコンパイルしたアプリをインストールできませんが、無いよりかましかと。 これで気になるコードを、Android端末だけでたくさん試すことができます。 私はEmacsユーザーなので、上記コマンドをEmacs上から生成して実行できるLispを作成して C-r キーで実行しています。 記事中のVSCodeは使ったことがないのですが、たしかVSCodeでもターミナル機能があったと思うので、同じように実行できるかと。
mojabi2025/03/26すみません。マニフェストと <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.app.installer"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Design.Light.NoActionBar"> <activity android:name=".MainActivity" android:launchMode="singleTop" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> </application> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> </manifest> providerです。 <?xml version="1.0" encoding="utf-8"?> <paths> <external-path path="Android/data/${applicationId}/" name="package_root" /> <cache-path path="Android/data/${applicationId}/" name="cache" /> <root-path path="." name="storage_root" /> </paths> これがないと権限関連が混乱する話になるところでした。。。
Discussion
Androidのアプリ開発はあまり詳しくないですが、非常に参考になりました。
aapt2の差し替えは、zip/unzipではうまくいかず、jar cf/jar xfで圧縮/解凍を行うと、期待通りの動作をしました。私の環境(Pixel9 Fold)では、
gradle.propertiesで-Xmx8g -Xms8gと設定しないと、aapt2が動作しませんでした。急いで作ったのでかなりベタベタですが、以下のような適当なスクリプトで最小限のプロジェクトを作って、いろいろいじって遊んでいます。
コメントありがとうございます!
zip/unzipではうまくいかない場合があるとは思い至りませんでした。ご共有くださいまして、ありがとうございます。
-Xmx8g -Xms8gはかなり大きな値ですね。。。Pixel Foldだと、Android端末上の開発も快適にできそうです。Androidプロジェクト自体をPythonから生成してしまうの、すごすぎます👀
返信ありがとうございます。
本当に貴重な記事をありがとうございます。
コンパイルからインストールまで、なんとかコマンドライン一発でできないかと思い、以下のようなアプリを作成しました。
文字列リソースは以下のような感じです。
上記を、例えばパッケージ名
com.example.app.installerに設定してコンパイルし、ファイルマネージャーなどからインストールします。起動後に画面上のボタンを押して「すべてのファイルへのアクセス」権限をオンにした状態で、Ubuntu on Termux から以下のコマンドを実行すると、Androidのインストーラー起動まではコマンド一発でいけます。
予め、
/storage/emulated/0/Termuxディレクトリを作成しておいてください。例えば
MyAppというプロジェクト内で実行すると、こんなかんじです。セキュリテイの制約で、何回か画面をタップしないとコンパイルしたアプリをインストールできませんが、無いよりかましかと。
これで気になるコードを、Android端末だけでたくさん試すことができます。
私はEmacsユーザーなので、上記コマンドをEmacs上から生成して実行できるLispを作成して
C-rキーで実行しています。記事中のVSCodeは使ったことがないのですが、たしかVSCodeでもターミナル機能があったと思うので、同じように実行できるかと。
すみません。マニフェストと
providerです。
これがないと権限関連が混乱する話になるところでした。。。