Zenn
📲

Android端末上でAndroidアプリをビルドする

に公開
4

はじめに

よく電車の中でコーディング&アプリビルドをしたくなる時があります。昨今のAndroidスマホは大きなメモリと高速なSoCを搭載しているので、十分にアプリ開発ができるはず。しかもこれだけ高性能化しているのに、ゲームくらいでしかその性能を存分に味わえないというのはもったいない(価格もPCと同等ともしくはそれ以上なのに...)!何よりも、手のひらでアプリ開発ができる、というロマンは素晴らしいものがあります。

しかし、意外とAndroid上でAndroid開発するための知見で、最新版に対応したものが少なく地味に苦労したため、備忘録も兼ねて記事にしたいと思います。

Android Studioは現状、直接Android端末にはインストールできません。Termux上などに構築したUbuntu Desktop上にAndroid StudioをGUIで入れるのもできなくはないそうですが、(ちょっと試したところ)挙動が不安定だったりモッサリしていたりするので、あまりベストな感じがしません。今回は、UbuntuをデスクトップなしでTermux上に構築し、その上にCLI上でAndroid環境を整え、コーディングはVSCodeで行なっていくようなスタイルでいきたいと思います。

流れ

  1. Termuxのインストール
  2. Ubuntuのインストール
  3. Android開発環境の構築
  4. aapt2の対応
  5. VSCodeのインストール
  6. おまけ

1. Termuxのインストール

Termuxは、Android用のターミナルエミュレータのアプリで、ルート化不要でLinux環境が使えます。
Play Storeにあるものはどうやら試験版のようで、あまり推奨されていなそうでした。Fdroid版またはGitHubから入れるのが良さそうだったので、自分はGitHub版のtermux-app_vXXX+github-debug_arm64-v8a.apkとなっているものを入れました。

Termuxにストレージへのアクセス許可

Termuxアプリを開き、以下のコマンドでスマホのストレージへのアクセスができるようにします。

Termux
termux-setup-storage

ポップアップまたは遷移した先の画面で、「許可」を押します。
これで、storage/フォルダとしてスマホのストレージの中身が認識されます。

Termux内の各種アップデート

Termux
apt update -y && apt upgrade -y && pkg update -y

(apt updateは、pkgコマンドでインストールを実行するときに自動で実行されるそうですが、念のため)

Termux内の各種インストール

pkgコマンドはTermuxに用意されたパッケージマネージャーで、aptのラッパーとのこと。下記は最小限のものだけ入れているので、必要に応じて追加してください。

Termux
pkg install wget proot -y

2. Ubuntuのインストール

Termux自体もLinuxを動かしているようですが、ファイル構造が違っていたりしてそのままLinuxとして色々いじるにはやや制約が多いとのことでした。(参考)そこで、なじみの深いUbuntuをいれます。
Andronixが配布しているインストーラーを使うと楽に構築できました。

Termux
wget https://raw.githubusercontent.com/AndronixApp/AndronixOrigin/master/Installer/Ubuntu22/ubuntu22.sh -O ubuntu22.sh
Termux
chmod +x ubuntu22.sh
Termux
./ubuntu22.sh

一度インストールした後は、Termuxで

Termux
./start-ubutnu22.sh

で起動できます。

Ubuntu内の各種アップデート

apt update -y && apt upgrade -y

Ubuntu内の各種インストール

こちらの記事内で使用する最小限のものになります。必要に応じて追加してください。

apt install nano dpkg gpg git zip unzip -y

gpgはVSCodeを入れる際の最後に、Microsoftの最新のリポジトリを入れたいか聞かれる場合があるのですが、yesと答えるときに入っていないとエラーが出るので入れました。(参考)

3. Android開発環境の構築

Android Studioは入れられないので、手動で環境を構築する必要があります。

open-jdkのインストール

apt install openjdk-17-jdk -y

現時点のAndroidProjectで最もよく見かけるバージョン17にて、ひとまずインストールしました。

コマンドラインツールのインストール

Google公式から、「コマンドラインツールのみ」の箇所に移動し、Linux用のcommandlinetools-linux-xxx_latest.zipとなっているファイルをダウンロードします。

スマホのストレージから、Ubuntu内のroot/androidフォルダに解凍します。(この時、Ubuntu内にいる場合はexitでTermuxに出る必要があります。)

Termux
unzip storage/downloads/commandlinetools-linux-xxx_latest.zip -d ubuntu22-fs/root/android

sdkmanagerを使えるようにする

Google公式のガイドの言う通りに、ubuntu22-fs/root/android/cmdline-tools直下にlatestフォルダを作成し、元々あったファイル群をその下に移動します。再度./start-ubuntu22.shでUbuntuに入り、下記のコマンドで移動させます。

mkdir android/cmdline-tools/latest
find android/cmdline-tools -mindepth 1 -maxdepth 1 -not -name "latest" -exec mv {} android/cmdline-tools/latest/ \;

※コマンドが複雑なのは、cmdline-tools配下全てだと移動先であるlatestフォルダも含まれてしまうのを考慮する必要があるため

PATHを通す

ANROID_HOMEなどの環境変数へのパスを通します。エディタはお好みで...

nano ~/.bashrc
~/.bashrc
...
# ファイルの一番下に追加
export ANDROID_HOME=$HOME/android
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
export PATH=$ANDROID_HOME/platform-tools:$PATH

なお、platform-toolsはこの後sdkmanagerで入れます。

変更後はbashを更新します。

source ~/.bashrc

以下のコマンドで/root/androidと出れば成功

echo $ANDROID_HOME

skdmanagerで各種インストール

パスが無事に通っていれば、下記のコマンドで無事にバージョン情報が確認できます。(12.0など)

sdkmanager --version

最新のプラットフォームツールと、APIレベル34(現時点で最新)のSDKツールをインストールします。

sdkmanager "platform-tools" "platforms;android-34"

残念ながら、emulatorは現時点ではArm64版はないようで、入れようとするとWarning: Failed to find package 'emulator'というエラーが出ました。

その後、下記のコマンドでライセンスを追加承認する必要があります。ひたすらyを押して承認していきます。

sdkmanager --licenses

4. aapt2の対応

試しにビルドしてみる

ここまできたら、一応Android自体のビルドはできるようになっているはずです。試しにnowinandroidをビルドしてみましょう。

StudioProjectsフォルダ配下に、nowinandroidをクローンします。

git clone https://github.com/android/nowinandroid.git ./StudioProjects/nowinandroid

ディレクトリ配下にそのままの名前でクローンするスマートな方法をご存知の方がいたらお教えください汗

その後、試しにdebugビルドしてみましょう。

cd StudioProjects/nowinandroid
./gradlew assembleDebug

./gradlewを使うと、自動でプロジェクトにあったバージョンのgradleのダウンロードが始まるの便利ですね〜

しかし、エラーが出る

残念ながら最後までビルドはできないはずです。なぜなら、Androidのアセット関連を扱うaapt2(Android Asset Packaging Tool)というツールが、Arm64アーキテクチャに対応していないからです。
下記のようなエラーが出るかと思います。

> Task :app:processDemoDebugResources FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:processDemoDebugResources'.   > Could not resolve all files for configuration ':app:demoDebugRuntimeClasspath'.
   > Failed to transform appcompat-1.7.0.aar (androidx.appcompat:appcompat:1.7.0) to match attributes {artifactType=android-compiled-dependencies-resources, org.gradle.category=library, org.gradle.dependency.bundling=external, org.gradle.libraryelements=aar, org.gradle.status=release, org.gradle.usage=java-runtime}.
      > Execution failed for AarResourcesCompilerTransform: /root/.gradle/caches/8.10/transforms/4a06d29f5dfb54242d37464d1409aabe/transformed/appcompat-1.7.0.
         > AAPT2 aapt2-8.6.1-11315950-linux Daemon #0: Daemon startup failed

Arm64版aapt2に置換する

これへの対処方法を、こちらのQiita記事で書いてくださっていました。なお、Arm64版aapt2の入手先が古くなってしまっていたので、新しい入手先を探して入手しました。

入手先がこちら↓
https://github.com/lzhiyong/android-sdk-tools/releases

android-sdk-tools-static-aarch64.zipというファイルをダウンロードして解凍すると、build-toolsフォルダから、aapt2のバイナリファイルが入手できます。自分はPCで解凍してから、aapt2ファイルだけをGoogleDrive経由でスマホに移動させました。

Termux
cp storage/downloads/aapt2 ubuntu22-fs/root/aapt2

このバイナリファイルを、Qiita記事のように、/root/.gradle/caches/modules-2/files-2.1/com.android.tools.build/aapt2配下にある、graldeバージョンごとのキャッシュディレクトリの下に、aapt2-xxx-linux.jarというファイルがあるので、その中のaapt2バイナリと置換します。
具体的には、aapt2-xxx-linux.jarを一旦zipにして解凍し、中身のaapt2を置換して、再度jarにして戻します。

置換処理を簡単に行えるようにするShellScriptを作ってみました。(このShellScriptでは、/root/.gradle/caches/modules-2/files-2.1/com.android.tools.build/aapt2配下の全てのaapt2-xxx-linux.jarを対象にaaptファイルを置換します。)
以下の内容をfix_aapt2.shというファイル名で保存して、Ubuntuのルートディレクトリに、aapt2ファイルと共に配置し、権限を与えてから実行するだけで置換が完了します。

コードを表示
fix_aapt2.sh
#!/bin/bash

set -e  # エラー時にスクリプトを停止

AAPT2_PATH=".gradle/caches/modules-2/files-2.1/com.android.tools.build/aapt2"
AAPT2_DIR="unzip-jar"

# 一時ディレクトリを作成
mkdir -p "$AAPT2_DIR"

# aapt2ディレクトリ以下のすべてのaapt2-*-linux.jarを処理
find "$AAPT2_PATH" -name "aapt2-*-linux.jar" | while read -r AAPT2_FULL_PATH; do
    AAPT2_BASENAME="$(basename "$AAPT2_FULL_PATH" .jar)"
    AAPT2_ZIP="$AAPT2_BASENAME.zip"

    echo "Processing: $AAPT2_FULL_PATH"

    # ファイルをコピーしてzipにリネーム
    cp "$AAPT2_FULL_PATH" "$AAPT2_ZIP"

    # 解凍
    unzip "$AAPT2_ZIP" -d "$AAPT2_DIR"

    # 元のzipファイルを削除
    rm "$AAPT2_ZIP"

    # aapt2を適切な位置にコピー
    cp "aapt2" "$AAPT2_DIR/aapt2"

    # zipを再作成
    (cd "$AAPT2_DIR" && zip "$AAPT2_ZIP" * && cd)

    # 修正後のzipを元の場所に戻す
    mv "$AAPT2_DIR/$AAPT2_ZIP" "$AAPT2_FULL_PATH"

    # 一時ディレクトリをクリア
    rm -rf "$AAPT2_DIR"/*

done
echo "All aapt2 files processed successfully."

aapt2fix_aapt2.shファイルをUbuntuのルートディレクトリに配置後、下記にて権限を付与。

chmod +x fix_aapt2.sh

権限付与後、下記にて実行。

./fix_aapt2.sh

再度ビルドを実行

置換が完了したら、再度./gradlew assembleDebugを実行します。

すると...無事にビルドが完了します!
自分の端末だと何度かTermuxが落ちて、そのたびに再実行する必要がありました。しかしちゃんと記録が蓄積されていくので、何度もやれば着実にビルドが進んでいきます。gradle.propertiesのJVMメモリの設定をいろいろいじると起きづらくなった気がします。8GBメモリのスマホなら-Xmx2500mなどの値(nowinandroidはデフォルトだと4GB)にしたらデバッグビルドだと一度も落ちませんでした。小さすぎるとそれはそれでエラーが出ました。

./gradlew assembleDebug
感動のBuild Successfulの文字

./gradlew assembleRelease
assembleReleaseもいけました

5. VSCodeのインストール

TermuxのCLI上でコーディングをするのはさすがにキツイため、モダンなエディタを使いたいところです。VSCodeをUbuntuに入れて、VSCode Serverとして使うのが一番良さそうでした。通常のWeb版VSCodeであるvscode.devだと何度やってもGitHubログインや一部プラグインのインストールができなかったのですが、Ubuntu上に立てたVSCode Serverだと、無事にできました。

公式サイトからArm64版.debパッケージをダウンロードします。(code_xxx_arm64.debという名前でした。amd64と紛らわしいので注意)
その後、以下のようにしてスマホのストレージからUbuntuに移動させます。Ubuntu内に入っている場合は、一旦exitコマンドでTermuxに出ておきます。

Termux
cp storage/downloads/code_*_arm64.deb ubuntu22-fs/root/code.deb

その後、Ubuntuを./start_ubuntu22.shで再度起動し、Ubuntu内にインストールします。

apt install ./code.deb -y

無事にインストールできたら、下記のコマンドで開始し、ブラウザでURLを開きます。

code serve-web

通常のスマホ画面だとせまくて要素がギチギチになってしまうので、Androidの設定アプリの「開発者向けオプション」→「最小幅」を、500dp〜600dpに設定すると画面が広々使えるようになりました。(この設定にしてからZenn記事もスマホ上で編集しやすくなりました。)

before: 411dp after: 520dp
dpi before dpi after

PWAとしてスマホのホーム画面に追加できるのが嬉しいところです。
vscode pwa

6. おまけ

端末のアーキテクチャを確認

Termux
pkg install neofetch
neofetch

これを実行すると下記のようなロマン溢れるASCIIアートと共に端末のアーキテクチャが確認できます(ただしデフォルトのTermuxのズームレベルだと見切れる可能性があるので、ピンチインしてから実行するのが良さそう)

termux_neofetch

TODO:

ビルドしたapkファイルをスムーズに端末に移動させて起動するフローを構築したいです。

おわりに

Android内でアプリ開発ができる、この感動は素晴らしいです。モバイル開発onモバイル端末の同志が他にいらっしゃいましたら、ぜひ繋がりたいです。
何か内容に間違いなどございましたら、コメントでご指摘くださいますと幸いです。

参考

https://dev.to/junaid_dev/setup-official-vs-code-on-android-5a

https://stackoverflow.com/questions/76763954/android-app-development-with-visual-studio-code

https://zenn.dev/link/comments/dd25e01c1d9426

https://issuetracker.google.com/issues/227219818

https://qiita.com/tacchi/items/ec63aa8bff5103982156

Discussion

mojabimojabi

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🐢turtlekazu🐢

コメントありがとうございます!
zip/unzipではうまくいかない場合があるとは思い至りませんでした。ご共有くださいまして、ありがとうございます。
-Xmx8g -Xms8gはかなり大きな値ですね。。。Pixel Foldだと、Android端末上の開発も快適にできそうです。
Androidプロジェクト自体をPythonから生成してしまうの、すごすぎます👀

mojabimojabi

返信ありがとうございます。
本当に貴重な記事をありがとうございます。

コンパイルからインストールまで、なんとかコマンドライン一発でできないかと思い、以下のようなアプリを作成しました。

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でもターミナル機能があったと思うので、同じように実行できるかと。

mojabimojabi

すみません。マニフェストと

<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>

これがないと権限関連が混乱する話になるところでした。。。

ログインするとコメントできます