diff --git a/apk/controller/.gitignore b/apk/controller/.gitignore new file mode 100644 index 0000000..907519a --- /dev/null +++ b/apk/controller/.gitignore @@ -0,0 +1,3 @@ +.gradle +app/.externalNativeBuild +app/build diff --git a/apk/controller/.gitmodules b/apk/controller/.gitmodules new file mode 100644 index 0000000..e1dce28 --- /dev/null +++ b/apk/controller/.gitmodules @@ -0,0 +1,3 @@ +[submodule "app/src/main/jni/hev-socks5-tunnel"] + path = app/src/main/jni/hev-socks5-tunnel + url = https://github.com/heiher/hev-socks5-tunnel diff --git a/apk/controller/.idea/.gitignore b/apk/controller/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/apk/controller/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/apk/controller/.idea/AndroidProjectSystem.xml b/apk/controller/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/apk/controller/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/assetWizardSettings.xml b/apk/controller/.idea/assetWizardSettings.xml new file mode 100644 index 0000000..fc877cc --- /dev/null +++ b/apk/controller/.idea/assetWizardSettings.xml @@ -0,0 +1,291 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/caches/deviceStreaming.xml b/apk/controller/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..2829725 --- /dev/null +++ b/apk/controller/.idea/caches/deviceStreaming.xml @@ -0,0 +1,1466 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/compiler.xml b/apk/controller/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/apk/controller/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/deploymentTargetSelector.xml b/apk/controller/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..1a29b48 --- /dev/null +++ b/apk/controller/.idea/deploymentTargetSelector.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/deviceManager.xml b/apk/controller/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/apk/controller/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/gradle.xml b/apk/controller/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/apk/controller/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/markdown.xml b/apk/controller/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/apk/controller/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/migrations.xml b/apk/controller/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/apk/controller/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/misc.xml b/apk/controller/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/apk/controller/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/runConfigurations.xml b/apk/controller/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/apk/controller/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/studiobot.xml b/apk/controller/.idea/studiobot.xml new file mode 100644 index 0000000..539e3b8 --- /dev/null +++ b/apk/controller/.idea/studiobot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/apk/controller/.idea/vcs.xml b/apk/controller/.idea/vcs.xml new file mode 100644 index 0000000..0f4e7ba --- /dev/null +++ b/apk/controller/.idea/vcs.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/apk/controller/LICENSE b/apk/controller/LICENSE new file mode 100644 index 0000000..5c5357c --- /dev/null +++ b/apk/controller/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 hev + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apk/controller/README.md b/apk/controller/README.md new file mode 100644 index 0000000..46b2b89 --- /dev/null +++ b/apk/controller/README.md @@ -0,0 +1,38 @@ +# IIAB-oA Controller + +**IIAB-oA Controller** is a specialized infrastructure component for the **Internet-in-a-Box (IIAB)** ecosystem on Android. It acts as a "Walled Garden" and persistent "Watchdog" designed to keep the Termux environment alive and accessible, even on devices with aggressive power management (e.g., Oppo/ColorOS, MIUI). + +## Core Capabilities + +### 🛡️ Master Watchdog (Supervision Layer) +An independent foreground service dedicated to environment stability: + +* **CPU & Wi-Fi Shield**: Prevents the system from putting Termux into Doze mode or disabling the Wi-Fi radio. +* **Heartbeat Pulse**: Sends a regulated API signal every 20 seconds to maintain process priority. +* **Zero-Config Protection**: Works independently of the VPN tunnel. + +### 🌐 Safe Pocket Web (Network Layer) +A high-performance VPN tunnel based on the tun2socks engine: + +* **Friendly URLs**: Routes traffic through internal IIAB services seamlessly. +* **Walled Garden**: Ensures a secure, filtered browsing environment. +* **Per-App Routing**: Granular control over which applications use the secure tunnel. + +### 🔒 Built-in Security +* **Biometric/PIN Lock**: Authentication is strictly required before the Watchdog or VPN can be disabled. +* **Safety Check**: Prevents activation if the device lacks a secure lock method (PIN/Pattern/Fingerprint), ensuring the user is never "locked out" of their own settings. + +## Acknowledgments +This project is a heavily customized spin-off of **[SocksTun](https://github.com/heiher/sockstun)** created by **[heiher](https://github.com/heiher)**. +All credit for the core native tunneling engine goes to the original author. This derivative has been re-architected to meet the specific requirements of the IIAB project. + +## Technical Details +* **Current Version**: v0.1.12alpha +* **License**: **MIT License** (See [LICENSE](LICENSE) for details). +* **Compatibility**: Android 8.0 (API 26) and above. + +## Disclaimer +This is a preview and demo published in the hope that it will be useful, but WITHOUT ANY WARRANTY. + +--- +*Maintained by IIAB Contributors - 2026* diff --git a/apk/controller/app/build.gradle b/apk/controller/app/build.gradle new file mode 100644 index 0000000..5610bce --- /dev/null +++ b/apk/controller/app/build.gradle @@ -0,0 +1,80 @@ +apply plugin: 'com.android.application' + +android { + namespace "org.iiab.controller" + compileSdkVersion 34 + ndkVersion "26.3.11579264" + + defaultConfig { + applicationId "org.iiab.controller" + minSdkVersion 24 + targetSdkVersion 34 + versionCode 21 + versionName "v0.1.25alpha-debug" + setProperty("archivesBaseName", "$applicationId-$versionName") + ndk { + abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" + } + externalNativeBuild { + ndkBuild { + arguments "APP_CFLAGS+=-DPKGNAME=org/iiab/controller -DCLSNAME=TProxyService -ffile-prefix-map=${rootDir}=." + arguments "APP_LDFLAGS+=-Wl,--build-id=none" + } + } + } + + signingConfigs { + release { + } + } + + buildTypes { + release { + minifyEnabled false + signingConfig signingConfigs.release + } + debug { + minifyEnabled false + signingConfig signingConfigs.release + } + } + + def propsFile = rootProject.file('store.properties') + def configName = 'release' + + if (propsFile.exists() && android.signingConfigs.hasProperty(configName)) { + def props = new Properties() + props.load(new FileInputStream(propsFile)) + if (props!=null && props.containsKey('storeFile')) { + android.signingConfigs[configName].storeFile = rootProject.file(props['storeFile']) + android.signingConfigs[configName].storePassword = props['storePassword'] + android.signingConfigs[configName].keyAlias = props['keyAlias'] + android.signingConfigs[configName].keyPassword = props['keyPassword'] + } + } + + externalNativeBuild { + ndkBuild { + path "src/main/jni/Android.mk" + } + } + + lintOptions { + checkReleaseBuilds false + // Or, if you prefer, you can continue to check for errors in release builds, + // but continue the build even when errors are found: + abortOnError false + } + + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.biometric:biometric:1.1.0' +} diff --git a/apk/controller/app/release/org.iiab.controller-v0.1.14alpha-release.apk b/apk/controller/app/release/org.iiab.controller-v0.1.14alpha-release.apk new file mode 100644 index 0000000..2fe06da Binary files /dev/null and b/apk/controller/app/release/org.iiab.controller-v0.1.14alpha-release.apk differ diff --git a/apk/controller/app/release/output-metadata.json b/apk/controller/app/release/output-metadata.json new file mode 100644 index 0000000..fed0783 --- /dev/null +++ b/apk/controller/app/release/output-metadata.json @@ -0,0 +1,21 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "org.iiab.controller", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 8, + "versionName": "v0.1.14alpha", + "outputFile": "org.iiab.controller-v0.1.14alpha-release.apk" + } + ], + "elementType": "File", + "minSdkVersionForDexing": 24 +} \ No newline at end of file diff --git a/apk/controller/app/src/main/AndroidManifest.xml b/apk/controller/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a8a1a7b --- /dev/null +++ b/apk/controller/app/src/main/AndroidManifest.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apk/controller/app/src/main/ic_launcher-playstore.png b/apk/controller/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..823e64e Binary files /dev/null and b/apk/controller/app/src/main/ic_launcher-playstore.png differ diff --git a/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java new file mode 100644 index 0000000..0df17d4 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java @@ -0,0 +1,225 @@ +/* + ============================================================================ + Name : AppListActivity.java + Author : hev + Copyright : Copyright (c) 2025 xyz + Description : App List Activity + ============================================================================ + */ + +package org.iiab.controller; + +import java.util.Set; +import java.util.List; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Comparator; +import java.util.Collections; +import java.util.ArrayList; + +import android.Manifest; +import android.os.Bundle; +import android.app.ListActivity; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.EditText; +import android.text.TextWatcher; +import android.text.Editable; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.ApplicationInfo; + +public class AppListActivity extends ListActivity { + private Preferences prefs; + private AppArrayAdapter adapter; + private boolean isChanged = false; + + private class Package { + public PackageInfo info; + public boolean selected; + public String label; + + public Package(PackageInfo info, boolean selected, String label) { + this.info = info; + this.selected = selected; + this.label = label; + } + } + + private class AppArrayAdapter extends ArrayAdapter { + private final List allPackages = new ArrayList(); + private final List filteredPackages = new ArrayList(); + private String lastFilter = ""; + + public AppArrayAdapter(Context context) { + super(context, R.layout.appitem); + } + + @Override + public void add(Package pkg) { + allPackages.add(pkg); + if (matchesFilter(pkg, lastFilter)) + filteredPackages.add(pkg); + notifyDataSetChanged(); + } + + @Override + public void clear() { + allPackages.clear(); + filteredPackages.clear(); + notifyDataSetChanged(); + } + + @Override + public void sort(Comparator cmp) { + Collections.sort(allPackages, (Comparator) cmp); + applyFilter(lastFilter); + } + + @Override + public int getCount() { + return filteredPackages.size(); + } + + @Override + public Package getItem(int position) { + return filteredPackages.get(position); + } + + public List getAllPackages() { + return allPackages; + } + + private boolean matchesFilter(Package pkg, String filter) { + if (filter == null || filter.length() == 0) + return true; + return pkg.label.toLowerCase().contains(filter.toLowerCase()); + } + + public void applyFilter(String filter) { + lastFilter = filter != null ? filter : ""; + filteredPackages.clear(); + if (lastFilter.length() == 0) { + filteredPackages.addAll(allPackages); + } else { + String f = lastFilter.toLowerCase(); + for (Package p : allPackages) { + if (p.label != null && p.label.toLowerCase().contains(f)) + filteredPackages.add(p); + } + } + notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View rowView = inflater.inflate(R.layout.appitem, parent, false); + ImageView imageView = (ImageView) rowView.findViewById(R.id.icon); + TextView textView = (TextView) rowView.findViewById(R.id.name); + CheckBox checkBox = (CheckBox) rowView.findViewById(R.id.checked); + + Package pkg = getItem(position); + PackageManager pm = getContext().getPackageManager(); + ApplicationInfo appinfo = pkg.info.applicationInfo; + imageView.setImageDrawable(appinfo.loadIcon(pm)); + textView.setText(appinfo.loadLabel(pm).toString()); + checkBox.setChecked(pkg.selected); + + return rowView; + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + + prefs = new Preferences(this); + Set apps = prefs.getApps(); + PackageManager pm = getPackageManager(); + adapter = new AppArrayAdapter(this); + + for (PackageInfo info : pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)) { + if (info.packageName.equals(getPackageName())) + continue; + if (info.requestedPermissions == null) + continue; + if (!Arrays.asList(info.requestedPermissions).contains(Manifest.permission.INTERNET)) + continue; + boolean selected = apps.contains(info.packageName); + String label = info.applicationInfo.loadLabel(pm).toString(); + Package pkg = new Package(info, selected, label); + adapter.add(pkg); + } + + EditText searchBox = new EditText(this); + searchBox.setHint("Search"); + int pad = (int) (8 * getResources().getDisplayMetrics().density); + searchBox.setPadding(pad, pad, pad, pad); + getListView().addHeaderView(searchBox, null, false); + + adapter.sort(new Comparator() { + public int compare(Package a, Package b) { + if (a.selected != b.selected) + return a.selected ? -1 : 1; + return a.label.compareTo(b.label); + } + }); + + setListAdapter(adapter); + + searchBox.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + adapter.applyFilter(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { } + }); + } + + @Override + protected void onDestroy() { + if (isChanged) { + Set apps = new HashSet(); + + for (Package pkg : adapter.getAllPackages()) { + if (pkg.selected) + apps.add(pkg.info.packageName); + } + + prefs.setApps(apps); + } + + super.onDestroy(); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + int headers = l.getHeaderViewsCount(); + int adjPos = position - headers; + if (adjPos < 0) + return; + Package pkg = adapter.getItem(adjPos); + pkg.selected = !pkg.selected; + CheckBox checkbox = (CheckBox) v.findViewById(R.id.checked); + if (checkbox != null) + checkbox.setChecked(pkg.selected); + isChanged = true; + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java b/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java new file mode 100644 index 0000000..e5fd896 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java @@ -0,0 +1,122 @@ +package org.iiab.controller; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * A stateless utility class to perform keep-alive actions for Termux. + * The lifecycle (start/stop/loop) is managed by the calling service. + */ +public class IIABWatchdog { + private static final String TAG = "IIAB-Controller"; + + public static final String ACTION_LOG_MESSAGE = "org.iiab.controller.LOG_MESSAGE"; + public static final String EXTRA_MESSAGE = "org.iiab.controller.EXTRA_MESSAGE"; + + // --- TEMPORARY DEBUG FLAGS --- + private static final boolean DEBUG_ENABLED = true; + private static final String BLACKBOX_FILE = "watchdog_heartbeat_log.txt"; + // ---------------------------- + + /** + * Performs a full heartbeat pulse: sending stimulus and debug ping. + */ + public static void performHeartbeat(Context context) { + sendStimulus(context); + performDebugPing(context); + } + + /** + * Sends a command to Termux to keep it active. + * This is the real keep-alive mechanism. + * @param context The context to use for sending the intent. + */ + public static void sendStimulus(Context context) { + if (DEBUG_ENABLED) { + writeToBlackBox(context, "Sending Intent (true) to Termux to keep it awake..."); + } + + Intent intent = new Intent("com.termux.service.RUN_COMMAND"); + intent.setClassName("com.termux", "com.termux.app.RunCommandService"); + intent.putExtra("com.termux.service.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/true"); + intent.putExtra("com.termux.service.RUN_COMMAND_BACKGROUND", true); + + try { + context.startService(intent); + } catch (Exception e) { + if (DEBUG_ENABLED) { + Log.e(TAG, "[DEBUG_DEEP_SLEEP] Failed to send 'true' command to Termux", e); + writeToBlackBox(context, "ERROR sending Intent: " + e.getMessage()); + } + } + } + + /** + * Pings the Termux NGINX server to check if it's responsive. + * This is a temporary debugging tool. + * // TODO: REMOVE AFTER HANS DEBUGGING + * @param context The context for writing to the blackbox log. + */ + public static void performDebugPing(Context context) { + final String NGINX_IP = "127.0.0.1"; + final int NGINX_PORT = 8085; + + new Thread(() -> { + try (Socket socket = new Socket()) { + socket.connect(new InetSocketAddress(NGINX_IP, NGINX_PORT), 2000); + if (DEBUG_ENABLED) { + Log.e(TAG, "[DEBUG_DEEP_SLEEP] PING 8085 SUCCESSFUL: Termux is alive."); + writeToBlackBox(context, "PING 8085 SUCCESSFUL: Termux is alive."); + } + } catch (IOException e) { + if (DEBUG_ENABLED) { + Log.w(TAG, "[DEBUG_DEEP_SLEEP] PING 8085 FAILED: " + e.getMessage()); + writeToBlackBox(context, "PING 8085 FAILED: " + e.getMessage()); + } + } + }).start(); + } + + public static void logSessionStart(Context context) { + if (DEBUG_ENABLED) { + writeToBlackBox(context, "HEARTBEAT SESSION STARTED (Thread based)"); + } + } + + public static void logSessionStop(Context context) { + if (DEBUG_ENABLED) { + writeToBlackBox(context, "HEARTBEAT SESSION STOPPED"); + } + } + + private static void writeToBlackBox(Context context, String message) { + try { + File logFile = new File(context.getFilesDir(), BLACKBOX_FILE); + FileWriter writer = new FileWriter(logFile, true); + String datePrefix = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + writer.append(datePrefix).append(" - ").append(message).append("\n"); + writer.close(); + + // Also broadcast for UI update + broadcastLog(context, message); + } catch (IOException e) { + Log.e(TAG, "Failed to write to BlackBox", e); + } + } + + private static void broadcastLog(Context context, String message) { + Intent intent = new Intent(ACTION_LOG_MESSAGE); + intent.putExtra(EXTRA_MESSAGE, message); + context.sendBroadcast(intent); + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java new file mode 100644 index 0000000..d6ef819 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -0,0 +1,540 @@ +/* + ============================================================================ + Name : MainActivity.java + Author : hev + Copyright : Copyright (c) 2023 xyz + Description : Main Activity + ============================================================================ + */ + +package org.iiab.controller; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.app.AlertDialog; +import android.content.Intent; +import android.content.Context; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import android.net.VpnService; +import android.net.Uri; +import android.text.method.ScrollingMovementMethod; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.biometric.BiometricManager; +import androidx.biometric.BiometricPrompt; +import androidx.core.content.ContextCompat; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.concurrent.Executor; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class MainActivity extends AppCompatActivity implements View.OnClickListener { + private Preferences prefs; + private EditText edittext_socks_addr; + private EditText edittext_socks_udp_addr; + private EditText edittext_socks_port; + private EditText edittext_socks_user; + private EditText edittext_socks_pass; + private EditText edittext_dns_ipv4; + private EditText edittext_dns_ipv6; + private CheckBox checkbox_udp_in_tcp; + private CheckBox checkbox_remote_dns; + private CheckBox checkbox_global; + private CheckBox checkbox_ipv4; + private CheckBox checkbox_ipv6; + private Button button_apps; + private Button button_save; + private Button button_control; + + private Button watchdogControl; + private TextView connectionLog; + private LinearLayout logActions; + private Button btnClearLog; + private Button btnCopyLog; + private LinearLayout configLayout; + private TextView configLabel; + private LinearLayout advancedConfig; + private TextView advConfigLabel; + private TextView logLabel; + private ImageButton themeToggle; + private TextView versionFooter; + + private BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(intent.getAction())) { + String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE); + addToLog(message); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + prefs = new Preferences(this); + setContentView(R.layout.main); + + edittext_socks_addr = (EditText) findViewById(R.id.socks_addr); + edittext_socks_udp_addr = (EditText) findViewById(R.id.socks_udp_addr); + edittext_socks_port = (EditText) findViewById(R.id.socks_port); + edittext_socks_user = (EditText) findViewById(R.id.socks_user); + edittext_socks_pass = (EditText) findViewById(R.id.socks_pass); + edittext_dns_ipv4 = (EditText) findViewById(R.id.dns_ipv4); + edittext_dns_ipv6 = (EditText) findViewById(R.id.dns_ipv6); + checkbox_ipv4 = (CheckBox) findViewById(R.id.ipv4); + checkbox_ipv6 = (CheckBox) findViewById(R.id.ipv6); + checkbox_global = (CheckBox) findViewById(R.id.global); + checkbox_udp_in_tcp = (CheckBox) findViewById(R.id.udp_in_tcp); + checkbox_remote_dns = (CheckBox) findViewById(R.id.remote_dns); + button_apps = (Button) findViewById(R.id.apps); + button_save = (Button) findViewById(R.id.save); + button_control = (Button) findViewById(R.id.control); + + watchdogControl = (Button) findViewById(R.id.watchdog_control); + watchdogControl.setOnClickListener(this); + + logActions = (LinearLayout) findViewById(R.id.log_actions); + btnClearLog = (Button) findViewById(R.id.btn_clear_log); + btnCopyLog = (Button) findViewById(R.id.btn_copy_log); + btnClearLog.setOnClickListener(this); + btnCopyLog.setOnClickListener(this); + + connectionLog = (TextView) findViewById(R.id.connection_log); + connectionLog.setMovementMethod(new ScrollingMovementMethod()); + // Enable text selection for copying large logs + connectionLog.setTextIsSelectable(true); + + configLayout = (LinearLayout) findViewById(R.id.config_layout); + configLabel = (TextView) findViewById(R.id.config_label); + configLabel.setOnClickListener(v -> toggleVisibility(configLayout, configLabel, "Configuration")); + + advancedConfig = (LinearLayout) findViewById(R.id.advanced_config); + advConfigLabel = (TextView) findViewById(R.id.adv_config_label); + advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, "Advanced Settings")); + + logLabel = (TextView) findViewById(R.id.log_label); + logLabel.setOnClickListener(v -> { + if (connectionLog.getVisibility() == View.GONE) { + readBlackBoxLogs(); // Load logs from file when expanding + } + toggleVisibility(connectionLog, logLabel, "Connection Log"); + logActions.setVisibility(connectionLog.getVisibility()); + }); + + themeToggle = (ImageButton) findViewById(R.id.theme_toggle); + themeToggle.setOnClickListener(v -> toggleTheme()); + applySavedTheme(); + + versionFooter = (TextView) findViewById(R.id.version_text); + setVersionFooter(); + + checkbox_udp_in_tcp.setOnClickListener(this); + checkbox_remote_dns.setOnClickListener(this); + checkbox_global.setOnClickListener(this); + button_apps.setOnClickListener(this); + button_save.setOnClickListener(this); + button_control.setOnClickListener(this); + updateUI(); + + /* Request VPN permission */ + Intent intent = VpnService.prepare(MainActivity.this); + if (intent != null) + startActivityForResult(intent, 0); + else + onActivityResult(0, RESULT_OK, null); + + checkBatteryOptimizations(); + addToLog("Application Started"); + } + + private void readBlackBoxLogs() { + File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); + if (!logFile.exists()) { + addToLog("--- No BlackBox file found ---"); + return; + } + + addToLog("--- Loading BlackBox Logs ---"); + try (BufferedReader br = new BufferedReader(new FileReader(logFile))) { + String line; + while ((line = br.readLine()) != null) { + addToLog("[FILE] " + line); + } + } catch (IOException e) { + addToLog("Error reading BlackBox: " + e.getMessage()); + } + addToLog("--- End of File ---"); + } + + private void setVersionFooter() { + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; + versionFooter.setText(version); + } catch (PackageManager.NameNotFoundException e) { + versionFooter.setText("v0.1.x"); + } + } + + @Override + protected void onResume() { + super.onResume(); + if (getIntent() != null && getIntent().getBooleanExtra(VpnRecoveryReceiver.EXTRA_RECOVERY, false)) { + addToLog("Recovery Pulse Received from System. Enforcing VPN..."); + Intent vpnIntent = new Intent(this, TProxyService.class); + startService(vpnIntent.setAction(TProxyService.ACTION_CONNECT)); + setIntent(null); + } + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } + + private void checkBatteryOptimizations() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) { + new AlertDialog.Builder(this) + .setTitle("Battery Optimization") + .setMessage("For the Watchdog to work reliably, please disable battery optimizations for this app.") + .setPositiveButton("Go to Settings", (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); + }) + .setNegativeButton("Cancel", null) + .show(); + } + } + } + + private void toggleTheme() { + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + int currentMode = AppCompatDelegate.getDefaultNightMode(); + int nextMode; + + if (currentMode == AppCompatDelegate.MODE_NIGHT_NO) { + nextMode = AppCompatDelegate.MODE_NIGHT_YES; + } else if (currentMode == AppCompatDelegate.MODE_NIGHT_YES) { + nextMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + } else { + nextMode = AppCompatDelegate.MODE_NIGHT_NO; + } + + SharedPreferences.Editor editor = sharedPref.edit(); + editor.putInt("ui_mode", nextMode); + editor.apply(); + + AppCompatDelegate.setDefaultNightMode(nextMode); + updateThemeToggleButton(nextMode); + } + + private void applySavedTheme() { + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + int savedMode = sharedPref.getInt("ui_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + AppCompatDelegate.setDefaultNightMode(savedMode); + updateThemeToggleButton(savedMode); + } + + private void updateThemeToggleButton(int mode) { + if (mode == AppCompatDelegate.MODE_NIGHT_NO) { + themeToggle.setImageResource(R.drawable.ic_theme_dark); + } else if (mode == AppCompatDelegate.MODE_NIGHT_YES) { + themeToggle.setImageResource(R.drawable.ic_theme_light); + } else { + themeToggle.setImageResource(R.drawable.ic_theme_system); + } + } + + private void toggleVisibility(View view, TextView label, String text) { + if (view.getVisibility() == View.GONE) { + view.setVisibility(View.VISIBLE); + label.setText("▼ " + text); + } else { + view.setVisibility(View.GONE); + label.setText("▶ " + text); + } + } + + @Override + protected void onStart() { + super.onStart(); + IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(logReceiver, filter, Context.RECEIVER_EXPORTED); + } else { + registerReceiver(logReceiver, filter); + } + } + + @Override + protected void onStop() { + super.onStop(); + try { + unregisterReceiver(logReceiver); + } catch (Exception e) { + // Ignore + } + } + + @Override + protected void onActivityResult(int request, int result, Intent data) { + super.onActivityResult(request, result, data); + if ((result == RESULT_OK) && prefs.getEnable()) { + Intent intent = new Intent(this, TProxyService.class); + startService(intent.setAction(TProxyService.ACTION_CONNECT)); + addToLog("VPN Permission Granted. Connecting..."); + } + } + + @Override + public void onClick(View view) { + if (view == checkbox_global || view == checkbox_remote_dns) { + savePrefs(); + updateUI(); + } else if (view == button_apps) { + startActivity(new Intent(this, AppListActivity.class)); + } else if (view == button_save) { + savePrefs(); + Context context = getApplicationContext(); + Toast.makeText(context, "Saved", Toast.LENGTH_SHORT).show(); + addToLog("Settings Saved"); + } else if (view.getId() == R.id.control) { + handleControlClick(); + } else if (view.getId() == R.id.watchdog_control) { + handleWatchdogClick(); + } else if (view.getId() == R.id.btn_clear_log) { + connectionLog.setText(""); + addToLog("Log reset"); + } else if (view.getId() == R.id.btn_copy_log) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString()); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toast.makeText(this, "Log copied to clipboard", Toast.LENGTH_SHORT).show(); + } + } + } + + private void handleWatchdogClick() { + boolean isEnabled = prefs.getWatchdogEnable(); + if (isEnabled) { + showWatchdogBiometricPrompt(); + } else { + toggleWatchdog(false); + } + } + + private void toggleWatchdog(boolean stop) { + prefs.setWatchdogEnable(!stop); + Intent intent = new Intent(this, WatchdogService.class); + if (stop) { + stopService(intent); + addToLog("Watchdog Stopping..."); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent.setAction(WatchdogService.ACTION_START)); + } else { + startService(intent.setAction(WatchdogService.ACTION_START)); + } + addToLog("Watchdog Starting..."); + } + updateUI(); + } + + private void handleControlClick() { + boolean isCurrentlyEnabled = prefs.getEnable(); + if (isCurrentlyEnabled) { + showBiometricPrompt(); + } else { + BiometricManager biometricManager = BiometricManager.from(this); + int authenticators = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) ? + (BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL) : + BiometricManager.Authenticators.BIOMETRIC_WEAK; + + boolean isSecure = false; + android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + if (km != null && km.isDeviceSecure()) isSecure = true; + + if (biometricManager.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS || isSecure) { + addToLog("User initiated connection"); + toggleService(false); + } else { + showEnrollmentDialog(); + } + } + } + + private void showEnrollmentDialog() { + new AlertDialog.Builder(this) + .setTitle("Security Required") + .setMessage("You must set up a PIN, Pattern, or Fingerprint on your device before enabling the secure environment.") + .setPositiveButton("Go to Settings", (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS); + startActivity(intent); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showBiometricPrompt() { + Executor executor = ContextCompat.getMainExecutor(this); + BiometricPrompt biometricPrompt = new BiometricPrompt(MainActivity.this, + executor, new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + addToLog("Authentication Success. Disconnecting..."); + toggleService(true); + } + }); + + BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder() + .setTitle("Authentication required") + .setSubtitle("Authenticate to disable the secure environment"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + promptBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | + BiometricManager.Authenticators.DEVICE_CREDENTIAL); + } else { + promptBuilder.setDeviceCredentialAllowed(true); + } + biometricPrompt.authenticate(promptBuilder.build()); + } + + private void showWatchdogBiometricPrompt() { + Executor executor = ContextCompat.getMainExecutor(this); + BiometricPrompt biometricPrompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + toggleWatchdog(true); + } + }); + + BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder() + .setTitle("Unlock Master Watchdog") + .setSubtitle("Authentication required to stop Termux protection"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + promptBuilder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | + BiometricManager.Authenticators.DEVICE_CREDENTIAL); + } else { + promptBuilder.setDeviceCredentialAllowed(true); + } + biometricPrompt.authenticate(promptBuilder.build()); + } + + private void toggleService(boolean isEnable) { + prefs.setEnable(!isEnable); + savePrefs(); + updateUI(); + Intent intent = new Intent(this, TProxyService.class); + if (isEnable) { + startService(intent.setAction(TProxyService.ACTION_DISCONNECT)); + addToLog("VPN Stopping..."); + } else { + startService(intent.setAction(TProxyService.ACTION_CONNECT)); + addToLog("VPN Starting..."); + } + } + + private void updateUI() { + boolean vpnActive = prefs.getEnable(); + boolean watchdogActive = prefs.getWatchdogEnable(); + + if (vpnActive) { + button_control.setText(R.string.control_disable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on)); + } else { + button_control.setText(R.string.control_enable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off)); + } + + if (watchdogActive) { + watchdogControl.setText("Disable Master Watchdog"); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); + } else { + watchdogControl.setText("Enable Master Watchdog"); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); + } + + edittext_socks_addr.setText(prefs.getSocksAddress()); + edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress()); + edittext_socks_port.setText(String.valueOf(prefs.getSocksPort())); + edittext_socks_user.setText(prefs.getSocksUsername()); + edittext_socks_pass.setText(prefs.getSocksPassword()); + edittext_dns_ipv4.setText(prefs.getDnsIpv4()); + edittext_dns_ipv6.setText(prefs.getDnsIpv6()); + checkbox_ipv4.setChecked(prefs.getIpv4()); + checkbox_ipv6.setChecked(prefs.getIpv6()); + checkbox_global.setChecked(prefs.getGlobal()); + checkbox_udp_in_tcp.setChecked(prefs.getUdpInTcp()); + checkbox_remote_dns.setChecked(prefs.getRemoteDns()); + + boolean editable = !vpnActive; + edittext_socks_addr.setEnabled(editable); + edittext_socks_port.setEnabled(editable); + button_save.setEnabled(editable); + } + + private void savePrefs() { + prefs.setSocksAddress(edittext_socks_addr.getText().toString()); + prefs.setSocksPort(Integer.parseInt(edittext_socks_port.getText().toString())); + prefs.setSocksUdpAddress(edittext_socks_udp_addr.getText().toString()); + prefs.setSocksUsername(edittext_socks_user.getText().toString()); + prefs.setSocksPassword(edittext_socks_pass.getText().toString()); + prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString()); + prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString()); + prefs.setIpv4(checkbox_ipv4.isChecked()); + prefs.setIpv6(checkbox_ipv6.isChecked()); + prefs.setGlobal(checkbox_global.isChecked()); + prefs.setUdpInTcp(checkbox_udp_in_tcp.isChecked()); + prefs.setRemoteDns(checkbox_remote_dns.isChecked()); + } + + private void addToLog(String message) { + runOnUiThread(() -> { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + String currentTime = sdf.format(new Date()); + String logEntry = "[" + currentTime + "] " + message + "\n"; + if (connectionLog != null) { + connectionLog.append(logEntry); + // Automatic scrolling to bottom + final int scrollAmount = connectionLog.getLayout() != null ? + connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight() : 0; + if (scrollAmount > 0) + connectionLog.scrollTo(0, scrollAmount); + } + }); + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java b/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java new file mode 100644 index 0000000..db563ed --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java @@ -0,0 +1,219 @@ +/* + ============================================================================ + Name : Preferences.java + Author : hev + Copyright : Copyright (c) 2023 xyz + Description : Preferences + ============================================================================ + */ + +package org.iiab.controller; + +import java.util.Set; +import java.util.HashSet; +import android.content.Context; +import android.content.SharedPreferences; + +public class Preferences +{ + public static final String PREFS_NAME = "SocksPrefs"; + public static final String SOCKS_ADDR = "SocksAddr"; + public static final String SOCKS_UDP_ADDR = "SocksUdpAddr"; + public static final String SOCKS_PORT = "SocksPort"; + public static final String SOCKS_USER = "SocksUser"; + public static final String SOCKS_PASS = "SocksPass"; + public static final String DNS_IPV4 = "DnsIpv4"; + public static final String DNS_IPV6 = "DnsIpv6"; + public static final String IPV4 = "Ipv4"; + public static final String IPV6 = "Ipv6"; + public static final String GLOBAL = "Global"; + public static final String UDP_IN_TCP = "UdpInTcp"; + public static final String REMOTE_DNS = "RemoteDNS"; + public static final String APPS = "Apps"; + public static final String ENABLE = "Enable"; + public static final String WATCHDOG_ENABLE = "WatchdogEnable"; + + private SharedPreferences prefs; + + public Preferences(Context context) { + prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_MULTI_PROCESS); + } + + public String getSocksAddress() { + return prefs.getString(SOCKS_ADDR, "127.0.0.1"); + } + + public void setSocksAddress(String addr) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(SOCKS_ADDR, addr); + editor.commit(); + } + + public String getSocksUdpAddress() { + return prefs.getString(SOCKS_UDP_ADDR, ""); + } + + public void setSocksUdpAddress(String addr) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(SOCKS_UDP_ADDR, addr); + editor.commit(); + } + + public int getSocksPort() { + return prefs.getInt(SOCKS_PORT, 1080); + } + + public void setSocksPort(int port) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(SOCKS_PORT, port); + editor.commit(); + } + + public String getSocksUsername() { + return prefs.getString(SOCKS_USER, ""); + } + + public void setSocksUsername(String user) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(SOCKS_USER, user); + editor.commit(); + } + + public String getSocksPassword() { + return prefs.getString(SOCKS_PASS, ""); + } + + public void setSocksPassword(String pass) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(SOCKS_PASS, pass); + editor.commit(); + } + + public String getDnsIpv4() { + return prefs.getString(DNS_IPV4, "8.8.8.8"); + } + + public void setDnsIpv4(String addr) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(DNS_IPV4, addr); + editor.commit(); + } + + public String getDnsIpv6() { + return prefs.getString(DNS_IPV6, "2001:4860:4860::8888"); + } + + public void setDnsIpv6(String addr) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putString(DNS_IPV6, addr); + editor.commit(); + } + + public String getMappedDns() { + return "198.18.0.2"; + } + + public boolean getUdpInTcp() { + return prefs.getBoolean(UDP_IN_TCP, false); + } + + public void setUdpInTcp(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(UDP_IN_TCP, enable); + editor.commit(); + } + + public boolean getRemoteDns() { + return prefs.getBoolean(REMOTE_DNS, true); + } + + public void setRemoteDns(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(REMOTE_DNS, enable); + editor.commit(); + } + + public boolean getIpv4() { + return prefs.getBoolean(IPV4, true); + } + + public void setIpv4(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(IPV4, enable); + editor.commit(); + } + + public boolean getIpv6() { + return prefs.getBoolean(IPV6, true); + } + + public void setIpv6(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(IPV6, enable); + editor.commit(); + } + + public boolean getGlobal() { + return prefs.getBoolean(GLOBAL, false); + } + + public void setGlobal(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(GLOBAL, enable); + editor.commit(); + } + + public Set getApps() { + return prefs.getStringSet(APPS, new HashSet()); + } + + public void setApps(Set apps) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(APPS, apps); + editor.commit(); + } + + public boolean getEnable() { + return prefs.getBoolean(ENABLE, false); + } + + public void setEnable(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(ENABLE, enable); + editor.commit(); + } + + public boolean getWatchdogEnable() { + return prefs.getBoolean(WATCHDOG_ENABLE, false); + } + + public void setWatchdogEnable(boolean enable) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(WATCHDOG_ENABLE, enable); + editor.commit(); + } + + public int getTunnelMtu() { + return 8500; + } + + public String getTunnelIpv4Address() { + return "198.18.0.1"; + } + + public int getTunnelIpv4Prefix() { + return 32; + } + + public String getTunnelIpv6Address() { + return "fc00::1"; + } + + public int getTunnelIpv6Prefix() { + return 128; + } + + public int getTaskStackSize() { + return 81920; + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java b/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java new file mode 100644 index 0000000..a81e052 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java @@ -0,0 +1,40 @@ +/* + ============================================================================ + Name : ServiceReceiver.java + Author : hev + Copyright : Copyright (c) 2023 xyz + Description : ServiceReceiver + ============================================================================ + */ + +package org.iiab.controller; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.VpnService; +import android.os.Build; + +public class ServiceReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { + Preferences prefs = new Preferences(context); + + /* Auto-start */ + if (prefs.getEnable()) { + Intent i = VpnService.prepare(context); + if (i != null) { + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(i); + } + i = new Intent(context, TProxyService.class); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(i.setAction(TProxyService.ACTION_CONNECT)); + } else { + context.startService(i.setAction(TProxyService.ACTION_CONNECT)); + } + } + } + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java b/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java new file mode 100644 index 0000000..397123e --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java @@ -0,0 +1,337 @@ +/* + ============================================================================ + Name : TProxyService.java + Author : hev + Copyright : Copyright (c) 2024 xyz + Description : TProxy Service + ============================================================================ + */ + +package org.iiab.controller; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Intent; +import android.net.VpnService; +import android.net.wifi.WifiManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ServiceInfo; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +public class TProxyService extends VpnService { + private static final String TAG = "IIAB-TProxy"; + private static native void TProxyStartService(String config_path, int fd); + private static native void TProxyStopService(); + private static native long[] TProxyGetStats(); + + public static final String ACTION_CONNECT = "org.iiab.controller.CONNECT"; + public static final String ACTION_DISCONNECT = "org.iiab.controller.DISCONNECT"; + public static final String ACTION_WATCHDOG_SYNC = "org.iiab.controller.WATCHDOG_SYNC"; + + private PowerManager.WakeLock wakeLock; + private WifiManager.WifiLock wifiLock; + + private Thread watchdogThread; + private volatile boolean isWatchdogRunning = false; + + static { + System.loadLibrary("hev-socks5-tunnel"); + } + + private ParcelFileDescriptor tunFd = null; + + @Override + public void onCreate() { + super.onCreate(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + String action = intent.getAction(); + if (ACTION_DISCONNECT.equals(action)) { + stopService(); + return START_NOT_STICKY; + } else if (ACTION_WATCHDOG_SYNC.equals(action)) { + syncWatchdogLocks(); + return START_STICKY; + } + } + startService(); + return START_STICKY; + } + + private void syncWatchdogLocks() { + Preferences prefs = new Preferences(this); + boolean watchdogEnabled = prefs.getWatchdogEnable(); + Log.d(TAG, "Syncing Watchdog state. Enabled: " + watchdogEnabled); + + if (watchdogEnabled) { + acquireLocks(); + startWatchdogLoop(); + } else { + stopWatchdogLoop(); + releaseLocks(); + } + } + + private void startWatchdogLoop() { + if (isWatchdogRunning) return; + + isWatchdogRunning = true; + IIABWatchdog.logSessionStart(this); + + watchdogThread = new Thread(() -> { + Log.i(TAG, "Watchdog Thread: Started loop"); + while (isWatchdogRunning) { + try { + // Perform the stimulus (The real keep-alive) + IIABWatchdog.sendStimulus(this); + + // Perform the debug ping (Checking if Termux is responsive) + IIABWatchdog.performDebugPing(this); + + // Sleep for 30 seconds + Thread.sleep(30000); + } catch (InterruptedException e) { + Log.i(TAG, "Watchdog Thread: Interrupted, stopping..."); + break; + } catch (Exception e) { + Log.e(TAG, "Watchdog Thread: Error in loop", e); + } + } + Log.i(TAG, "Watchdog Thread: Loop ended"); + }); + watchdogThread.setName("IIAB-Watchdog-Thread"); + watchdogThread.start(); + } + + private void stopWatchdogLoop() { + isWatchdogRunning = false; + if (watchdogThread != null) { + watchdogThread.interrupt(); + watchdogThread = null; + } + IIABWatchdog.logSessionStop(this); + } + + private void acquireLocks() { + try { + if (wakeLock == null) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "IIAB:TProxyWakeLock"); + wakeLock.acquire(); + Log.i(TAG, "CPU WakeLock acquired under VPN shield"); + } + if (wifiLock == null) { + WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "IIAB:TProxyWifiLock"); + wifiLock.acquire(); + Log.i(TAG, "Wi-Fi Lock acquired under VPN shield"); + } + } catch (Exception e) { + Log.e(TAG, "Error acquiring locks", e); + } + } + + private void releaseLocks() { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + wakeLock = null; + Log.i(TAG, "CPU WakeLock released"); + } + if (wifiLock != null && wifiLock.isHeld()) { + wifiLock.release(); + wifiLock = null; + Log.i(TAG, "Wi-Fi Lock released"); + } + } + + @Override + public void onDestroy() { + stopWatchdogLoop(); + releaseLocks(); + super.onDestroy(); + } + + @Override + public void onRevoke() { + super.onRevoke(); + } + + public void startService() { + if (tunFd != null) { + syncWatchdogLocks(); + return; + } + + Preferences prefs = new Preferences(this); + + /* VPN */ + String session = new String(); + VpnService.Builder builder = new VpnService.Builder(); + builder.setBlocking(false); + builder.setMtu(prefs.getTunnelMtu()); + if (prefs.getIpv4()) { + String addr = prefs.getTunnelIpv4Address(); + int prefix = prefs.getTunnelIpv4Prefix(); + String dns = prefs.getDnsIpv4(); + builder.addAddress(addr, prefix); + builder.addRoute("0.0.0.0", 0); + if (!prefs.getRemoteDns() && !dns.isEmpty()) + builder.addDnsServer(dns); + session += "IPv4"; + } + if (prefs.getIpv6()) { + String addr = prefs.getTunnelIpv6Address(); + int prefix = prefs.getTunnelIpv6Prefix(); + String dns = prefs.getDnsIpv6(); + builder.addAddress(addr, prefix); + builder.addRoute("::", 0); + if (!prefs.getRemoteDns() && !dns.isEmpty()) + builder.addDnsServer(dns); + if (!session.isEmpty()) + session += " + "; + session += "IPv6"; + } + if (prefs.getRemoteDns()) { + builder.addDnsServer(prefs.getMappedDns()); + } + boolean disallowSelf = true; + if (prefs.getGlobal()) { + session += "/Global"; + } else { + for (String appName : prefs.getApps()) { + try { + builder.addAllowedApplication(appName); + disallowSelf = false; + } catch (NameNotFoundException e) { + } + } + session += "/per-App"; + } + if (disallowSelf) { + String selfName = getApplicationContext().getPackageName(); + try { + builder.addDisallowedApplication(selfName); + } catch (NameNotFoundException e) { + } + } + builder.setSession(session); + tunFd = builder.establish(); + if (tunFd == null) { + stopSelf(); + return; + } + + /* TProxy */ + File tproxy_file = new File(getCacheDir(), "tproxy.conf"); + try { + tproxy_file.createNewFile(); + FileOutputStream fos = new FileOutputStream(tproxy_file, false); + + String tproxy_conf = "misc:\n" + + " task-stack-size: " + prefs.getTaskStackSize() + "\n" + + "tunnel:\n" + + " mtu: " + prefs.getTunnelMtu() + "\n"; + + tproxy_conf += "socks5:\n" + + " port: " + prefs.getSocksPort() + "\n" + + " address: '" + prefs.getSocksAddress() + "'\n" + + " udp: '" + (prefs.getUdpInTcp() ? "tcp" : "udp") + "'\n"; + + if (!prefs.getSocksUdpAddress().isEmpty()) { + tproxy_conf += " udp-address: '" + prefs.getSocksUdpAddress() + "'\n"; + } + + if (!prefs.getSocksUsername().isEmpty() && + !prefs.getSocksPassword().isEmpty()) { + tproxy_conf += " username: '" + prefs.getSocksUsername() + "'\n"; + tproxy_conf += " password: '" + prefs.getSocksPassword() + "'\n"; + } + + if (prefs.getRemoteDns()) { + tproxy_conf += "mapdns:\n" + + " address: " + prefs.getMappedDns() + "\n" + + " port: 53\n" + + " network: 240.0.0.0\n" + + " netmask: 240.0.0.0\n" + + " cache-size: 10000\n"; + } + + fos.write(tproxy_conf.getBytes()); + fos.close(); + } catch (IOException e) { + return; + } + TProxyStartService(tproxy_file.getAbsolutePath(), tunFd.getFd()); + prefs.setEnable(true); + + String channelName = "socks5"; + initNotificationChannel(channelName); + createNotification(channelName); + + // Start loop and locks if enabled + syncWatchdogLocks(); + } + + public void stopService() { + if (tunFd == null) + return; + + stopWatchdogLoop(); + releaseLocks(); + stopForeground(true); + + /* TProxy */ + TProxyStopService(); + + /* VPN */ + try { + tunFd.close(); + } catch (IOException e) { + } + tunFd = null; + + System.exit(0); + } + + private void createNotification(String channelName) { + Intent i = new Intent(this, MainActivity.class); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent pi = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Builder notification = new NotificationCompat.Builder(this, channelName); + Notification notify = notification + .setContentTitle(getString(R.string.app_name)) + .setSmallIcon(android.R.drawable.sym_def_app_icon) + .setContentIntent(pi) + .build(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(1, notify); + } else { + startForeground(1, notify, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE); + } + } + + // create NotificationChannel + private void initNotificationChannel(String channelName) { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.app_name); + NotificationChannel channel = new NotificationChannel(channelName, name, NotificationManager.IMPORTANCE_DEFAULT); + notificationManager.createNotificationChannel(channel); + } + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java b/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java new file mode 100644 index 0000000..709db7e --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java @@ -0,0 +1,68 @@ +package org.iiab.controller; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; +import androidx.core.app.NotificationCompat; + +public class VpnRecoveryReceiver extends BroadcastReceiver { + private static final String TAG = "IIAB-VpnRecovery"; + public static final String EXTRA_RECOVERY = "recovery_mode"; + private static final String CHANNEL_ID = "recovery_channel"; + private static final int NOTIFICATION_ID = 911; + + @Override + public void onReceive(Context context, Intent intent) { + if ("org.iiab.controller.RECOVER_VPN".equals(intent.getAction())) { + Log.i(TAG, "Boomerang Signal Received! Triggering high-priority recovery..."); + + Preferences prefs = new Preferences(context); + if (prefs.getEnable()) { + showRecoveryNotification(context); + } + } + } + + private void showRecoveryNotification(Context context) { + NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, "VPN Recovery", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + if (manager != null) manager.createNotificationChannel(channel); + } + + Intent uiIntent = new Intent(context, MainActivity.class); + uiIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + uiIntent.putExtra(EXTRA_RECOVERY, true); + + PendingIntent pendingIntent = PendingIntent.getActivity( + context, 0, uiIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setContentTitle("Safe Pocket Web Interrupted") + .setContentText("Tap to restore secure environment immediately.") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_ALARM) + .setAutoCancel(true) + .setOngoing(true) + .setFullScreenIntent(pendingIntent, true) // High priority request to open + .setContentIntent(pendingIntent); + + if (manager != null) { + manager.notify(NOTIFICATION_ID, builder.build()); + } + } +} diff --git a/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java b/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java new file mode 100644 index 0000000..12133e0 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java @@ -0,0 +1,143 @@ +package org.iiab.controller; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.os.SystemClock; +import android.util.Log; +import androidx.core.app.NotificationCompat; + +public class WatchdogService extends Service { + private static final String CHANNEL_ID = "watchdog_channel"; + private static final int NOTIFICATION_ID = 2; + + public static final String ACTION_START = "org.iiab.controller.WATCHDOG_START"; + public static final String ACTION_STOP = "org.iiab.controller.WATCHDOG_STOP"; + public static final String ACTION_HEARTBEAT = "org.iiab.controller.HEARTBEAT"; + + private static final int HEARTBEAT_INTERVAL_MS = 20 * 1000; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + String action = intent.getAction(); + if (ACTION_START.equals(action)) { + startWatchdog(); + } else if (ACTION_STOP.equals(action)) { + stopWatchdog(); + return START_NOT_STICKY; + } else if (ACTION_HEARTBEAT.equals(action)) { + IIABWatchdog.performHeartbeat(this); + // CRITICAL: Reschedule for the next pulse to create an infinite loop + scheduleHeartbeat(); + } + } + return START_STICKY; + } + + private void startWatchdog() { + Notification notification = createNotification(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground(NOTIFICATION_ID, notification); + } else { + startForeground(NOTIFICATION_ID, notification); + } + + IIABWatchdog.logSessionStart(this); + scheduleHeartbeat(); + } + + private void stopWatchdog() { + cancelHeartbeat(); + IIABWatchdog.logSessionStop(this); + stopForeground(true); + stopSelf(); + } + + private PendingIntent getHeartbeatPendingIntent() { + Intent intent = new Intent(this, WatchdogService.class); + intent.setAction(ACTION_HEARTBEAT); + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags |= PendingIntent.FLAG_IMMUTABLE; + } + return PendingIntent.getService(this, 0, intent, flags); + } + + private void scheduleHeartbeat() { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = getHeartbeatPendingIntent(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // This wakes up the device even in Doze Mode + alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + HEARTBEAT_INTERVAL_MS, + pendingIntent); + } else { + alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + HEARTBEAT_INTERVAL_MS, + pendingIntent); + } + } + + private void cancelHeartbeat() { + AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + PendingIntent pendingIntent = getHeartbeatPendingIntent(); + if (alarmManager != null) { + alarmManager.cancel(pendingIntent); + } + } + + @Override + public void onDestroy() { + stopWatchdog(); + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, "IIAB Watchdog Service", + NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Ensures services remain active when screen is off."); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + private Notification createNotification() { + Intent notificationIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, + PendingIntent.FLAG_IMMUTABLE); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("IIAB Watchdog Active") + .setContentText("Protecting Termux environment...") + .setSmallIcon(android.R.drawable.ic_lock_idle_lock) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setOngoing(true) + .build(); + } +} diff --git a/apk/controller/app/src/main/jni/Android.mk b/apk/controller/app/src/main/jni/Android.mk new file mode 100644 index 0000000..548efc9 --- /dev/null +++ b/apk/controller/app/src/main/jni/Android.mk @@ -0,0 +1,16 @@ +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +include $(call all-subdir-makefiles) diff --git a/apk/controller/app/src/main/jni/Application.mk b/apk/controller/app/src/main/jni/Application.mk new file mode 100644 index 0000000..505d0f4 --- /dev/null +++ b/apk/controller/app/src/main/jni/Application.mk @@ -0,0 +1,21 @@ +# Copyright (C) 2023 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +APP_OPTIM := release +APP_PLATFORM := android-29 +APP_ABI := armeabi-v7a arm64-v8a +APP_CFLAGS := -O3 -DPKGNAME=hev/sockstun +APP_CPPFLAGS := -O3 -std=c++11 +NDK_TOOLCHAIN_VERSION := clang diff --git a/apk/controller/app/src/main/jni/hev-socks5-tunnel b/apk/controller/app/src/main/jni/hev-socks5-tunnel new file mode 160000 index 0000000..4d6c334 --- /dev/null +++ b/apk/controller/app/src/main/jni/hev-socks5-tunnel @@ -0,0 +1 @@ +Subproject commit 4d6c334dbfb68a79d1970c2744e62d09f71df12f diff --git a/apk/controller/app/src/main/res/drawable/ic_launcher_background.xml b/apk/controller/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apk/controller/app/src/main/res/drawable/ic_launcher_foreground.xml b/apk/controller/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..274f969 --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/apk/controller/app/src/main/res/drawable/ic_theme_dark.xml b/apk/controller/app/src/main/res/drawable/ic_theme_dark.xml new file mode 100644 index 0000000..ef032cf --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/ic_theme_dark.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/apk/controller/app/src/main/res/drawable/ic_theme_light.xml b/apk/controller/app/src/main/res/drawable/ic_theme_light.xml new file mode 100644 index 0000000..e031529 --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/ic_theme_light.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/apk/controller/app/src/main/res/drawable/ic_theme_system.xml b/apk/controller/app/src/main/res/drawable/ic_theme_system.xml new file mode 100644 index 0000000..9bee5e3 --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/ic_theme_system.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/apk/controller/app/src/main/res/drawable/rounded_button.xml b/apk/controller/app/src/main/res/drawable/rounded_button.xml new file mode 100644 index 0000000..04b6403 --- /dev/null +++ b/apk/controller/app/src/main/res/drawable/rounded_button.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/apk/controller/app/src/main/res/layout/appitem.xml b/apk/controller/app/src/main/res/layout/appitem.xml new file mode 100644 index 0000000..06dcf42 --- /dev/null +++ b/apk/controller/app/src/main/res/layout/appitem.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/apk/controller/app/src/main/res/layout/main.xml b/apk/controller/app/src/main/res/layout/main.xml new file mode 100644 index 0000000..609fe1d --- /dev/null +++ b/apk/controller/app/src/main/res/layout/main.xml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + +