One day in April, I landed a freelance gig to batch-generate Android APK packages. The requirement was that upon opening the app, it would display different background images to showcase the main character’s formidable professional capabilities.

Since I had never written native Android before—at most using React Native for cross-platform apps—manually creating a bunch of projects and modifying them by hand would be pure manual labor, which was definitely out of the question.

LLM First

Lately, I’ve been using LLMs to solve repetitive tasks, so I directly asked an LLM how to write this thing.

1
假如你是安卓和Bash脚本开发专家,我需要通过Bash命令生成不定数量的安卓安装包,每个安卓安装包都有不同的图标和图片,请写出Bash脚本。

Although GPT-4 wrote a lot of Bash script content, errors still popped up in the minor details, so I fed the error messages back to the LLM to resolve them.

However, keep in mind that longer conversations aren’t necessarily better. Sometimes, when the raw error messages and code get too long, severe hallucinations occur. Therefore, after writing a bit, running a bit, and verifying a bit, I cleared the context and asked anew.

Learning Android and Bash Together

Finally, after relentless debugging by the LLM and human effort (just about 2 hours), a runnable Bash script was produced:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/bin/bash

PROJECTS_COUNT=1

for i in $(seq 1 $PROJECTS_COUNT); do
PROJECT_NAME="Dual_Record_$(openssl rand -hex 3)"
PACKAGE_NAME="com.example.$PROJECT_NAME"

# 创建基础的项目结构
mkdir -p "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME"
mkdir -p "./$PROJECT_NAME/app/src/main/res/layout"
mkdir -p "./$PROJECT_NAME/app/src/main/res/mipmap"
echo "rootProject.name='$PROJECT_NAME'" > "./$PROJECT_NAME/settings.gradle"
echo "include ':app'" > "./$PROJECT_NAME/settings.gradle"

# 复制图标到项目中
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher.png"
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher_round.png"

# 创建strings.xml文件并添加app_name字符串资源
mkdir -p "./$PROJECT_NAME/app/src/main/res/values/"
cat > "./$PROJECT_NAME/app/src/main/res/values/strings.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APP名字</string>
</resources>
EOF

# Inside the loop, setting PACKAGE_NAME and PROJECT_NAME

# This goes in the project-level build.gradle creation step
cat > "./$PROJECT_NAME/build.gradle" <<EOF
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Make sure to use a compatible version
classpath 'com.android.tools.build:gradle:7.4.2'
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
EOF

# Adjusted AndroidManifest.xml creation with android:exported="true"
cat > "./$PROJECT_NAME/app/src/main/AndroidManifest.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<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.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity"
android:exported="true"> <!-- Ensure this line is added -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
EOF

# 创建一个简单的MainActivity
cat > "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME/MainActivity.java" <<EOF
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);
}
}
EOF

# 创建一个简单的activity_main.xml布局文件
cat > "./$PROJECT_NAME/app/src/main/res/layout/activity_main.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="欢迎使用XX系统_$PROJECT_NAME!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
EOF

# 创建app模块的build.gradle文件
cat > "./$PROJECT_NAME/app/build.gradle" <<EOF
apply plugin: 'com.android.application'

android {
compileSdkVersion 31
defaultConfig {
applicationId "$PACKAGE_NAME"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
EOF

# 初始化Gradle Wrapper
cd "$PROJECT_NAME"
gradle wrapper --gradle-version=7.5
cd ..

# 构建项目
cd "$PROJECT_NAME"
./gradlew assembleDebug
cd ..

# 复制APK到脚本执行目录
cp ./"$PROJECT_NAME"/app/build/outputs/apk/debug/app-debug.apk ./"$PROJECT_NAME.apk"

echo "Project $PROJECT_NAME has been created and packaged."
done

echo "Finished creating and packaging $PROJECTS_COUNT projects."

Actually, I haven’t written scripts with Bash in a long time, so I used this project as an opportunity to learn~

1
#!/bin/bash

The first line specifies the location of the bash interpreter, which is generally read when executing ./xxx.sh.

1
PROJECTS_COUNT=1

The second line sets a variable to specify the number of projects to generate.

1
2
3
for i in $(seq 1 $PROJECTS_COUNT); do
…………
done

Next, a for statement is written. seq 1 \$PROJECTS_COUNT generates a sequence from 1 to PROJECTS_COUNT. The $ symbol in front packages the expression inside the parentheses into a variable, and finally, the specific logic is written between do and done.

1
2
PROJECT_NAME="Dual_Record_$(openssl rand -hex 3)"
PACKAGE_NAME="com.example.$PROJECT_NAME"

Since each package name cannot be repeated, openssl rand -hex 3 is used to generate a string of random characters, and the result is appended directly to the end of the string.

1
2
3
4
5
6
# 创建基础的项目结构
mkdir -p "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME"
mkdir -p "./$PROJECT_NAME/app/src/main/res/layout"
mkdir -p "./$PROJECT_NAME/app/src/main/res/mipmap"
echo "rootProject.name='$PROJECT_NAME'" > "./$PROJECT_NAME/settings.gradle"
echo "include ':app'" > "./$PROJECT_NAME/settings.gradle"

Android or Java projects generally need to follow fixed project directory conventions, so mkdir -p is required in advance to directly create the directory hierarchy. After creation, project information is written into the settings.gradle project configuration.

1
2
3
# 复制图标到项目中
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher.png"
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher_round.png"

Use the cp command to copy the custom icon to the target folder using the specified filename.

1
2
3
4
5
6
7
8
# 创建strings.xml文件并添加app_name字符串资源
mkdir -p "./$PROJECT_NAME/app/src/main/res/values/"
cat > "./$PROJECT_NAME/app/src/main/res/values/strings.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APP名字</string>
</resources>
EOF

Use cat > "file" <<EOF to write multi-line strings to the specified file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# This goes in the project-level build.gradle creation step
cat > "./$PROJECT_NAME/build.gradle" <<EOF
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Make sure to use a compatible version
classpath 'com.android.tools.build:gradle:7.4.2'
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
EOF

build.gradle specifies the actions to be executed when compiling the project. Note that the Gradle version should match the local machine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Adjusted AndroidManifest.xml creation with android:exported="true"
cat > "./$PROJECT_NAME/app/src/main/AndroidManifest.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<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.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity"
android:exported="true"> <!-- Ensure this line is added -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
EOF

Write core Android information into AndroidManifest.xml, including icon, theme, and other details.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    # 创建一个简单的MainActivity
cat > "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME/MainActivity.java" <<EOF
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);
}
}
EOF

MainActivity.java is the application’s entry point; displaying the main view here completes the work.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    # 创建一个简单的activity_main.xml布局文件
cat > "./$PROJECT_NAME/app/src/main/res/layout/activity_main.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="欢迎使用XX系统_$PROJECT_NAME!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
EOF

Even the simplest Android application needs a layout set up, which is specified in activity_main.xml. Here, only a TextView is set up for display.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 创建app模块的build.gradle文件
cat > "./$PROJECT_NAME/app/build.gradle" <<EOF
apply plugin: 'com.android.application'

android {
compileSdkVersion 31
defaultConfig {
applicationId "$PACKAGE_NAME"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
EOF

The submodule’s build.gradle configures the Android compile version and dependency information; a simple application like this won’t depend on too much.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 初始化Gradle Wrapper
cd "$PROJECT_NAME"
gradle wrapper --gradle-version=7.5
cd ..

# 构建项目
cd "$PROJECT_NAME"
./gradlew assembleDebug
cd ..

# 复制APK到脚本执行目录
cp ./"$PROJECT_NAME"/app/build/outputs/apk/debug/app-debug.apk ./"$PROJECT_NAME.apk"

echo "Project $PROJECT_NAME has been created and packaged."

Finally, enter the directory, use Gradle to package and build the project, and copy the APK to the Bash execution directory for convenient collection, upload, and deployment later.

At this point, a Bash script capable of batch-generating Android APK packages was complete. By scripting it, I had LLMs and machines working for me, saving a ton of time.

To outsiders, the effort and scale look huge, so you can ask for a higher price. Tool automation really gives side hustles a major boost.