Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions controller/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@
adjustPan, so the IME covered the ExtraKeys bar and the text stuck. adjustResize shrinks the
window so the extra-keys bar + terminal sit above the keyboard. -->

<!-- ADFA-5192: the Debian terminal re-homed out of MainActivity. Same window/IME setup as
MainActivity carried the terminal (adjustResize + configChanges); singleTop lets the
keep-alive notification's SINGLE_TOP intent reuse the running instance. Not exported:
launched only in-app (redesign Settings, the terminal's own notification PendingIntent). -->
<activity
android:name=".TerminalActivity"
android:exported="false"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />

<activity
android:name=".PortalActivity"
android:exported="false"
Expand Down
127 changes: 127 additions & 0 deletions controller/app/src/main/java/org/iiab/controller/TerminalActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* ============================================================================
* Name : TerminalActivity.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : ADFA-5192. Dedicated host for the Debian terminal, re-homed out of the
* legacy MainActivity so that Activity (and the dead legacy tabbed UI) can
* be removed. Thin by design: it loads the native termux engine, hosts the
* terminal_bottom_sheet layout, and delegates everything to TerminalController
* (which owns the TerminalView, sessions, drawer and extra keys). The two
* Activity-side callbacks TerminalController needs — addToLog / vibrateDevice —
* are implemented here.
*
* Behaviour-preserving: the terminal opens full-screen and a swipe-down on the
* sheet finishes back to the caller (the redesign), exactly as the old
* terminal-only mode did in MainActivity.
* ============================================================================
*/
package org.iiab.controller;

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.google.android.material.bottomsheet.BottomSheetBehavior;

public class TerminalActivity extends AppCompatActivity implements TerminalController.Host {

// Load native C++ engine (termux). Was previously loaded by MainActivity's static block.
static {
System.loadLibrary("termux");
}

private TerminalController terminalController;
private boolean finishOnHideAttached = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.terminal_activity);

terminalController = new TerminalController(this, this);
terminalController.bind();
openTerminal();
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
// Reused via FLAG_ACTIVITY_SINGLE_TOP (e.g. the keep-alive notification): re-expand.
openTerminal();
}

private void openTerminal() {
if (terminalController == null) return;
View root = findViewById(R.id.terminal_coordinator);
Runnable open = () -> {
terminalController.openFullTerminal();
// Attach finish-on-hide only after the sheet has been expanded, so the initial
// HIDDEN state (set in bind()) can never reach a live callback and close the
// Activity on launch. Mirrors MainActivity, which attached it inside the open.
attachFinishOnHide();
};
if (root != null) root.post(open);
else open.run();
}

/**
* Swiping the terminal sheet down finishes back to the caller instead of exposing an
* empty surface. Mirrors MainActivity.attachTerminalOnlyFinish (ADFA-4987): no peek
* stop — a swipe-down goes straight to HIDDEN -> finish(). Attached once (after the
* first expand); a re-open via onNewIntent must not stack a second callback.
*/
private void attachFinishOnHide() {
if (finishOnHideAttached) return;
View sheet = findViewById(R.id.terminal_bottom_sheet);
if (sheet == null) return;
BottomSheetBehavior<View> b = BottomSheetBehavior.from(sheet);
b.setHideable(true);
b.setSkipCollapsed(true);
b.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
finish();
}
}

@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) { }
});
finishOnHideAttached = true;
}

@Override
protected void onDestroy() {
// Release the terminal UI delegate so the app-scoped session store never holds a
// destroyed Activity; running sessions keep going (ADFA-4696).
if (terminalController != null) terminalController.detach();
super.onDestroy();
}

// --- TerminalController.Host ---------------------------------------------
@Override
public void addToLog(String message) {
// Single source of truth; the Usage console observes LogRepository (ADFA-4640).
LogRepository.get().append(message);
}

@Override
public void vibrateDevice() {
android.os.Vibrator v = (android.os.Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (v != null && v.hasVibrator()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
v.vibrate(android.os.VibrationEffect.createOneShot(50, android.os.VibrationEffect.DEFAULT_AMPLITUDE));
} else {
v.vibrate(50);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,10 @@ CHANNEL_ID, getString(R.string.terminal_channel_name),
}

private Notification buildNotification() {
// Tapping the notification opens the terminal directly, bypassing the hidden
// version-footer gesture (ADFA-4696). SINGLE_TOP reuses the running Activity.
Intent open = new Intent(this, MainActivity.class)
.putExtra(MainActivity.EXTRA_OPEN_TERMINAL, true)
.putExtra(MainActivity.EXTRA_TERMINAL_ONLY, true)
// Tapping the notification opens the terminal directly (ADFA-4696). ADFA-5192: the terminal
// is its own Activity now, so no terminal-only extras are needed. SINGLE_TOP reuses the
// running instance.
Intent open = new Intent(this, TerminalActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pending = PendingIntent.getActivity(
this, 0, open, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,12 +205,9 @@ private void openAppSettings(Context ctx) {
} catch (Exception ignore) { /* no-op */ }
}

/** The full Debian terminal lives in MainActivity; EXTRA_OPEN_TERMINAL opens it directly. */
/** The full Debian terminal lives in its own TerminalActivity (ADFA-5192). */
private void openTerminal(Context ctx) {
Intent i = new Intent(ctx, org.iiab.controller.MainActivity.class);
i.putExtra(org.iiab.controller.MainActivity.EXTRA_OPEN_TERMINAL, true);
i.putExtra(org.iiab.controller.MainActivity.EXTRA_TERMINAL_ONLY, true);
ctx.startActivity(i);
ctx.startActivity(new Intent(ctx, org.iiab.controller.TerminalActivity.class));
}

// ---- Advanced (power-user features — preview for now) ----
Expand Down
117 changes: 117 additions & 0 deletions controller/app/src/main/res/layout/terminal_activity.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
ADFA-5192: the Debian terminal re-homed out of the legacy MainActivity into its own
TerminalActivity. This is the terminal_bottom_sheet subtree lifted verbatim from
res/layout/main.xml (drag handle + drawer + TerminalView + extra keys), kept as a
BottomSheetBehavior child of a CoordinatorLayout so TerminalController.bind() —
which does BottomSheetBehavior.from(terminal_bottom_sheet) — works unchanged.
The terminal is a legitimately always-dark surface (like the QR black/white), so the
hardcoded dark hex is intentional and carried over as-is; restyling to M3 tokens is
out of scope for a behaviour-preserving re-home.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/terminal_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000">

<LinearLayout
android:id="@+id/terminal_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000"
app:behavior_hideable="true"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

<FrameLayout
android:id="@+id/terminal_drag_handle_area"
android:layout_width="match_parent"
android:layout_height="32dp"
android:background="#111111"
android:clickable="true"
android:focusable="true">

<LinearLayout
android:layout_width="80dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:background="#222222"
android:gravity="center"
android:orientation="vertical">

<View
android:layout_width="32dp"
android:layout_height="2dp"
android:layout_marginBottom="4dp"
android:background="#555555" />

<View
android:layout_width="32dp"
android:layout_height="2dp"
android:background="#555555" />
</LinearLayout>
</FrameLayout>

<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">

<com.termux.view.TerminalView
android:id="@+id/terminal_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<LinearLayout
android:id="@+id/left_drawer"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#1A1A1A"
android:orientation="vertical"
android:elevation="8dp">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sessions_title"
android:textColor="#FFFFFF"
android:textSize="14sp"
android:textStyle="bold"
android:padding="16dp"
android:background="#111111" />

<ListView
android:id="@+id/left_drawer_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:choiceMode="singleChoice"
android:divider="#333333"
android:dividerHeight="1dp" />

<Button
android:id="@+id/new_session_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:backgroundTint="#333333"
android:textColor="#FFFFFF"
android:text="@string/new_session_btn"
android:textStyle="bold" />
</LinearLayout>

</androidx.drawerlayout.widget.DrawerLayout>

<com.termux.shared.termux.extrakeys.ExtraKeysView
android:id="@+id/extra_keys_view"
android:layout_width="match_parent"
android:layout_height="86dp"
android:background="#1E1E1E"
android:visibility="visible" />
</LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
Loading