diff --git a/apk/controller/app/build.gradle b/apk/controller/app/build.gradle
index ad5941b..63cdfe4 100644
--- a/apk/controller/app/build.gradle
+++ b/apk/controller/app/build.gradle
@@ -79,4 +79,6 @@ dependencies {
implementation 'androidx.biometric:biometric:1.1.0'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.webkit:webkit:1.12.0'
+ // ZXing for QR Code generation
+ implementation 'com.google.zxing:core:3.5.2'
}
diff --git a/apk/controller/app/src/main/AndroidManifest.xml b/apk/controller/app/src/main/AndroidManifest.xml
index aa5905a..3940303 100644
--- a/apk/controller/app/src/main/AndroidManifest.xml
+++ b/apk/controller/app/src/main/AndroidManifest.xml
@@ -42,13 +42,17 @@
-
+
+
+
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
index 530968e..51d2c93 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java
@@ -122,6 +122,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private String serverTransitionText = "";
private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper());
private Runnable timeoutRunnable;
+ private boolean isWifiActive = false;
+ private boolean isHotspotActive = false;
private String currentTargetUrl = null;
private long pulseStartTime = 0;
@@ -267,6 +269,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
deckContainer = findViewById(R.id.deck_container);
btnServerControl = findViewById(R.id.btn_server_control);
+ ImageButton btnShareQr = findViewById(R.id.btn_share_qr);
dashboardManager = new DashboardManager(this, findViewById(android.R.id.content), () -> {
handleControlClick();
@@ -335,6 +338,23 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
});
+ // --- QR Share Button Logic ---
+ btnShareQr.setOnClickListener(v -> {
+ if (!isServerAlive) {
+ // Rule 1: Server must be running
+ Snackbar.make(findViewById(android.R.id.content), R.string.qr_error_no_server, Snackbar.LENGTH_LONG).show();
+ return;
+ }
+ if (!isWifiActive && !isHotspotActive) {
+ // Rule 2: At least one network must be active
+ Snackbar.make(findViewById(android.R.id.content), R.string.qr_error_no_network, Snackbar.LENGTH_LONG).show();
+ return;
+ }
+
+ // Launch the new QrActivity
+ startActivity(new Intent(MainActivity.this, QrActivity.class));
+ });
+
connectionLog.setMovementMethod(new ScrollingMovementMethod());
connectionLog.setTextIsSelectable(true);
connectionLog.setOnTouchListener((v, event) -> {
@@ -1003,8 +1023,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/iiab-termux");
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{actionFlag});
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
- intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
- intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
+ intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/env");
+ intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{
+ "INTENT_MODE=headless",
+ "/data/data/com.termux/files/usr/bin/bash",
+ "/data/data/com.termux/files/usr/bin/iiab-termux",
+ actionFlag
+ });
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -1043,6 +1068,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} catch (Exception ex) {}
}
+ // Store states for the QR button logic
+ this.isWifiActive = isWifiOn;
+ this.isHotspotActive = isHotspotOn;
+
// Let the Dashboard handle the LEDs!
if (dashboardManager != null) dashboardManager.updateConnectivityLeds(isWifiOn, isHotspotOn);
}
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java
new file mode 100644
index 0000000..2e9c479
--- /dev/null
+++ b/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java
@@ -0,0 +1,184 @@
+package org.iiab.controller;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.List;
+
+public class QrActivity extends AppCompatActivity {
+
+ private TextView titleText;
+ private TextView ipText;
+ private ImageView qrImageView;
+ private ImageButton btnFlip;
+ private View cardContainer;
+
+ private String wifiIp = null;
+ private String hotspotIp = null;
+ private boolean showingWifi = true; // Tracks which network is currently displayed
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_qr); // Rename your dialog_qr.xml to this
+
+ titleText = findViewById(R.id.qr_network_title);
+ ipText = findViewById(R.id.qr_ip_text);
+ qrImageView = findViewById(R.id.qr_image_view);
+ btnFlip = findViewById(R.id.btn_flip_qr);
+ cardContainer = findViewById(R.id.qr_card_container);
+ Button btnClose = findViewById(R.id.btn_close_qr);
+
+ // Improve 3D perspective to avoid visual clipping during rotation
+ float distance = 8000 * getResources().getDisplayMetrics().density;
+ cardContainer.setCameraDistance(distance);
+
+ btnClose.setOnClickListener(v -> finish());
+
+ btnFlip.setOnClickListener(v -> {
+ // Disable button during animation to prevent spam
+ btnFlip.setEnabled(false);
+ animateCardFlip();
+ });
+
+ // 1. Fetch real physical IPs with strict interface naming
+ fetchNetworkInterfaces();
+
+ // 2. Determine initial state and button visibility
+ if (wifiIp != null && hotspotIp != null) {
+ btnFlip.setVisibility(View.VISIBLE); // Both active, enable flipping
+ showingWifi = true;
+ } else if (wifiIp != null) {
+ btnFlip.setVisibility(View.GONE);
+ showingWifi = true;
+ } else if (hotspotIp != null) {
+ btnFlip.setVisibility(View.GONE);
+ showingWifi = false;
+ } else {
+ // Fallback just in case they died between the MainActivity click and this onCreate
+ finish();
+ return;
+ }
+
+ updateQrDisplay();
+ }
+
+ /**
+ * Performs a 3D flip animation. Swaps the data halfway through when the card is invisible.
+ */
+ private void animateCardFlip() {
+ // Phase 1: Rotate out (0 to 90 degrees)
+ ObjectAnimator flipOut = ObjectAnimator.ofFloat(cardContainer, "rotationY", 0f, 90f);
+ flipOut.setDuration(200); // 200ms
+ flipOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Card is edge-on (invisible). Swap the data!
+ showingWifi = !showingWifi;
+ updateQrDisplay();
+
+ // Phase 2: Rotate in from the other side (-90 to 0 degrees)
+ cardContainer.setRotationY(-90f);
+ ObjectAnimator flipIn = ObjectAnimator.ofFloat(cardContainer, "rotationY", -90f, 0f);
+ flipIn.setDuration(200);
+ flipIn.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ btnFlip.setEnabled(true); // Unlock button
+ }
+ });
+ flipIn.start();
+ }
+ });
+ flipOut.start();
+ }
+
+ /**
+ * Updates the UI text and generates the new QR Code
+ */
+ private void updateQrDisplay() {
+ String currentIp = showingWifi ? wifiIp : hotspotIp;
+ String title = showingWifi ? getString(R.string.qr_title_wifi) : getString(R.string.qr_title_hotspot);
+
+ // 8085 is the default port for the IIAB interface
+ String url = "http://" + currentIp + ":8085";
+
+ titleText.setText(title);
+ ipText.setText(url);
+
+ Bitmap qrBitmap = generateQrCode(url);
+ if (qrBitmap != null) {
+ qrImageView.setImageBitmap(qrBitmap);
+ }
+ }
+
+ /**
+ * Strictly categorizes network interfaces to avoid Hotspot being labeled as Wi-Fi.
+ */
+ private void fetchNetworkInterfaces() {
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ String name = intf.getName();
+ if (!intf.isUp()) continue;
+
+ // Strict categorizations
+ boolean isStrictWifi = name.equals("wlan0");
+ boolean isHotspot = name.startsWith("ap") || name.startsWith("swlan") || name.equals("wlan1") || name.equals("wlan2");
+
+ if (isStrictWifi || isHotspot) {
+ List addrs = Collections.list(intf.getInetAddresses());
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
+ if (isStrictWifi) wifiIp = addr.getHostAddress();
+ if (isHotspot) hotspotIp = addr.getHostAddress();
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) { }
+ }
+
+ /**
+ * Generates a pure Black & White Bitmap using ZXing.
+ */
+ private Bitmap generateQrCode(String text) {
+ QRCodeWriter writer = new QRCodeWriter();
+ try {
+ // 800x800 guarantees high resolution on any screen
+ BitMatrix bitMatrix = writer.encode(text, BarcodeFormat.QR_CODE, 800, 800);
+ int width = bitMatrix.getWidth();
+ int height = bitMatrix.getHeight();
+ Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ bmp.setPixel(x, y, bitMatrix.get(x, y) ? Color.BLACK : Color.WHITE);
+ }
+ }
+ return bmp;
+ } catch (WriterException e) {
+ return null;
+ }
+ }
+}
diff --git a/apk/controller/app/src/main/res/layout/activity_qr.xml b/apk/controller/app/src/main/res/layout/activity_qr.xml
new file mode 100644
index 0000000..94c94ec
--- /dev/null
+++ b/apk/controller/app/src/main/res/layout/activity_qr.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apk/controller/app/src/main/res/layout/main.xml b/apk/controller/app/src/main/res/layout/main.xml
index 7ae5401..9ea37d8 100644
--- a/apk/controller/app/src/main/res/layout/main.xml
+++ b/apk/controller/app/src/main/res/layout/main.xml
@@ -31,6 +31,19 @@
android:textSize="18sp"
android:textStyle="bold" />
+
+
\n\nXiaomi detected: Please set battery saver to \'No restrictions\' in settings.
For the app to work 100%, please disable battery optimization.
FIX
+
+ Please start the server to share over the network.
+ Please enable Wi-Fi or Hotspot to share over the network.
+ Wi-Fi Network
+ Hotspot Network
+ Switch Network
\ No newline at end of file
diff --git a/apk/controller/app/src/main/res/values/themes.xml b/apk/controller/app/src/main/res/values/themes.xml
index 46ae9e3..b0ffb64 100644
--- a/apk/controller/app/src/main/res/values/themes.xml
+++ b/apk/controller/app/src/main/res/values/themes.xml
@@ -11,4 +11,10 @@
- @color/section_body_bg_light
- @color/section_header_bg
+