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
91 changes: 88 additions & 3 deletions app/src/main/java/com/anland/termux/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import android.graphics.Insets;
import android.hardware.display.DisplayManager;
import android.content.SharedPreferences;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
Expand Down Expand Up @@ -69,6 +72,8 @@ public class MainActivity extends Activity
// Latency presets in ms; 0 = engine default. Shared with SettingsActivity.
static final String KEY_SPEAKER_LATENCY_MS = "speaker_latency_ms";
static final String KEY_MIC_LATENCY_MS = "mic_latency_ms";
// Audio keep-alive toggle. Shared with SettingsActivity.
static final String KEY_AUDIO_KEEPALIVE = "audio_keepalive";
private static final int REQ_RECORD_AUDIO = 1001;
private static final int REQ_CAMERA = 1002;
// Camera service fds/threads are created once and persist across reconnects;
Expand All @@ -77,6 +82,10 @@ public class MainActivity extends Activity
private ICompatibleBridge compatibleBridge;
private boolean compatibleFdReady = false;
private boolean compatibleReceiverRegistered = false;
// Media audio focus keeps volume controls and Android audio policy aligned with
// the AAudio playback stream while this window is in the foreground.
private AudioManager mAudioManager;
private AudioFocusRequest mAudioFocusRequest;
private static final String DEFAULT_SOCKET_PATH = "/data/data/com.termux/files/usr/tmp/anland/display_daemon.sock";
private static final String KEY_ACCESSIBILITY_ENABLED = "accessibility_key_intercept";
private static final String KEY_EXTRA_KEYS_ENABLED = "extra_keys_bar";
Expand Down Expand Up @@ -128,6 +137,9 @@ public class MainActivity extends Activity
private final float[] mCapturedTouchpadResolvedDelta = new float[2];
private final SparseArray<Float> mButtonDragLastX = new SparseArray<>();
private final SparseArray<Float> mButtonDragLastY = new SparseArray<>();
// Single-button clickpads report every physical press as BUTTON_PRIMARY. Keep
// the resolved Linux button latched for the duration of that press.
private int mLastTouchpadButtonPressed = 0;
private String mDisplayCutoutMode = DisplayCutoutMode.HIDE_ALL;
private boolean mPipTransitionPending = false;
private boolean mVirtualKeyboardVisibleBeforePip = false;
Expand Down Expand Up @@ -285,6 +297,7 @@ private void stopNative() {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setupMediaAudio();
applyScreenOrientation();

sInstance = this;
Expand Down Expand Up @@ -624,11 +637,47 @@ private void setupCursorHiding() {
surfaceView.setPointerIcon(PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
}

/* Bind hardware volume controls and media focus to the same usage as the
* AAudio output stream. This prevents Android audio policy from throttling
* short Linux UI sounds while the desktop window is foregrounded. */
private void setupMediaAudio() {
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mAudioManager = getSystemService(AudioManager.class);
if (mAudioManager == null)
return;

AudioAttributes attrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
mAudioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attrs)
.setWillPauseWhenDucked(false)
.setOnAudioFocusChangeListener(change ->
Log.i(TAG, "audio focus change: " + change))
.build();
}

private void requestMediaAudioFocus() {
if (mAudioManager == null || mAudioFocusRequest == null)
return;
int result = mAudioManager.requestAudioFocus(mAudioFocusRequest);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
Log.w(TAG, "media audio focus not granted: " + result);
}

private void abandonMediaAudioFocus() {
if (mAudioManager != null && mAudioFocusRequest != null)
mAudioManager.abandonAudioFocusRequest(mAudioFocusRequest);
}

@Override
protected void onResume() {
super.onResume();
mPipTransitionPending = false;

requestMediaAudioFocus();

String displayCutoutMode = DisplayCutoutMode.get(
getSharedPreferences(PREFS_NAME, MODE_PRIVATE));
if (!mDisplayCutoutMode.equals(displayCutoutMode)) {
Expand Down Expand Up @@ -694,6 +743,7 @@ && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
pushRefreshRate();
applyMicState();
applyAudioLatency();
applyAudioKeepalive();
}

// ===== 重新读取触摸板设置 =====
Expand Down Expand Up @@ -807,6 +857,7 @@ protected void onPause() {
dm.unregisterDisplayListener(displayListener);
if (!mPipTransitionPending && !isInPictureInPictureMode())
stopNative();
abandonMediaAudioFocus();
}

private boolean hasPipPermission() {
Expand Down Expand Up @@ -853,6 +904,7 @@ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode,

@Override
protected void onDestroy() {
abandonMediaAudioFocus();
stopNative();
if (compatibleReceiverRegistered) {
unregisterReceiver(compatibleBridgeReceiver);
Expand Down Expand Up @@ -906,6 +958,11 @@ private void applyAudioLatency() {
Native.nativeSetAudioLatency(speakerMs, micMs);
}

private void applyAudioKeepalive() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
Native.nativeSetAudioKeepalive(prefs.getBoolean(KEY_AUDIO_KEEPALIVE, false));
}

private void applyMicState() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
boolean want = prefs.getBoolean(KEY_MIC_ENABLED, false);
Expand Down Expand Up @@ -964,6 +1021,7 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig
pushRefreshRate();
applyMicState();
applyAudioLatency();
applyAudioKeepalive();

// ===== 更新屏幕尺寸并重置平滑状态 =====
virtualTouchpad.onSurfaceChanged();
Expand Down Expand Up @@ -1527,10 +1585,12 @@ private boolean handleCapturedPointerEvent(MotionEvent event) {
sendCapturedScrollAxes(event, -1);
}

if (action == MotionEvent.ACTION_CANCEL)
if (action == MotionEvent.ACTION_CANCEL) {
mLastTouchpadButtonPressed = 0;
releaseAllMouseButtons();
else
updateMouseButtonStateFromEvent(event);
} else {
updateTouchpadButtonStateFromEvent(event);
}
return true;
}

Expand Down Expand Up @@ -1828,6 +1888,7 @@ private void resetCapturedTouchpadGesture() {
mCapturedTouchpadLastCentroidY = 0f;
mButtonDragLastX.clear();
mButtonDragLastY.clear();
mLastTouchpadButtonPressed = 0;
}

private int capturedPointerViewWidth() {
Expand Down Expand Up @@ -1950,6 +2011,30 @@ private void updateMouseButtonStateFromEvent(MotionEvent event) {
updateMouseButtonState(effectiveButtonState(event));
}

/** Resolve clickpad primary presses to left/right from the slowest contact. */
private void updateTouchpadButtonStateFromEvent(MotionEvent event) {
int action = event.getActionMasked();
int buttonState = event.getButtonState();
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
int button = mCapturedTouchpad != null
? mCapturedTouchpad.clickpadButton(event) : 0x110;
mLastTouchpadButtonPressed = button == 0x111
? MotionEvent.BUTTON_SECONDARY : MotionEvent.BUTTON_PRIMARY;
buttonState &= ~(MotionEvent.BUTTON_PRIMARY | MotionEvent.BUTTON_SECONDARY);
buttonState |= mLastTouchpadButtonPressed;
} else if (action == MotionEvent.ACTION_BUTTON_RELEASE) {
// Release the button chosen at press time even if the finger drifted.
buttonState &= ~(MotionEvent.BUTTON_PRIMARY | MotionEvent.BUTTON_SECONDARY);
mLastTouchpadButtonPressed = 0;
} else if (mLastTouchpadButtonPressed != 0) {
buttonState &= ~(MotionEvent.BUTTON_PRIMARY | MotionEvent.BUTTON_SECONDARY);
buttonState |= mLastTouchpadButtonPressed;
} else {
buttonState = effectiveButtonState(event);
}
updateMouseButtonState(buttonState);
}

private void updateMouseButtons(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL)
releaseAllMouseButtons();
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/anland/termux/Native.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ public static native void nativeConfigure(String socketPath, boolean useRoot,
public static native void nativeSendTextInput(byte[] data);
public static native void nativeSetMicEnabled(boolean enabled);
public static native void nativeSetAudioLatency(int speakerMs, int micMs);
public static native void nativeSetAudioKeepalive(boolean enabled);
}
20 changes: 20 additions & 0 deletions app/src/main/java/com/anland/termux/SettingsActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class SettingsActivity extends Activity {
private static final String KEY_USE_ROOT = "use_root";
private static final String KEY_MIC_ENABLED = "mic_enabled";
private static final String KEY_CAMERA_ENABLED = "camera_enabled";
private static final String KEY_AUDIO_KEEPALIVE = "audio_keepalive";
private static final String KEY_SPEAKER_LATENCY_MS = "speaker_latency_ms";
private static final String KEY_MIC_LATENCY_MS = "mic_latency_ms";
private static final String KEY_ACCESSIBILITY_ENABLED = "accessibility_key_intercept";
Expand Down Expand Up @@ -943,6 +944,25 @@ private void addConnectionSection(LinearLayout root) {
cameraHint.setPadding(0, dp(4), 0, 0);
root.addView(cameraHint);

// Keep the output stream hot for short Linux UI sounds, or let it
// idle-stop after silence to save standby power. Disabled by default.
Switch keepaliveSwitch = new Switch(this);
keepaliveSwitch.setText(R.string.audio_keepalive_switch);
keepaliveSwitch.setTextSize(14);
keepaliveSwitch.setPadding(0, dp(16), 0, 0);
keepaliveSwitch.setChecked(prefs.getBoolean(KEY_AUDIO_KEEPALIVE, false));
keepaliveSwitch.setOnCheckedChangeListener((v, checked) ->
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
.putBoolean(KEY_AUDIO_KEEPALIVE, checked).apply());
root.addView(keepaliveSwitch);

TextView keepaliveHint = new TextView(this);
keepaliveHint.setText(R.string.audio_keepalive_hint);
keepaliveHint.setTextSize(12);
keepaliveHint.setTextColor(getColor(R.color.settings_text_secondary));
keepaliveHint.setPadding(0, dp(4), 0, 0);
root.addView(keepaliveHint);

// Audio latency presets, separately for the speaker (playback) and microphone
// (capture) paths. The chosen buffer is forwarded to the producer's PipeWire
// nodes; smaller = lower latency but more risk of audio glitches.
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/java/com/anland/termux/Touchpad.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ interface Output {
private float startX1, startY1;
private float lastX2, lastY2;
private long downTime1;
// Kept across cancel() so a physical clickpad press can inspect the contact
// positions that were maintained by the preceding gesture events.
private boolean clickpadBaselineValid = false;
private final float touchSlop;

private boolean isSingleTapCandidate = false;
Expand Down Expand Up @@ -230,6 +233,46 @@ void cancel() {
lastTapY = 0f;
}

/**
* Resolve a clickpad's generic primary button to Linux left/right. The contact
* that moved least is treated as the pressing finger and its horizontal
* position selects the half of the pad. Coordinates are compared in the same
* output space used by gesture recognition.
*/
int clickpadButton(MotionEvent event) {
if (!clickpadBaselineValid || event.getPointerCount() <= 0)
return 0x110; // BTN_LEFT
int limit = Math.min(event.getPointerCount(), 2);
int slowestIndex = 0;
float slowestSpeed = Float.MAX_VALUE;
for (int i = 0; i < limit; i++) {
float lastX = i == 0 ? lastX1 : lastX2;
float lastY = i == 0 ? lastY1 : lastY2;
float curX = toOutputX(event.getX(i));
float curY = toOutputY(event.getY(i));
float dx = curX - lastX;
float dy = curY - lastY;
float speed = dx * dx + dy * dy;
if (speed < slowestSpeed) {
slowestSpeed = speed;
slowestIndex = i;
}
}
float curX = toOutputX(event.getX(slowestIndex));
float midpoint = outputWidth > 0 ? outputWidth / 2f : curX;
return curX < midpoint ? 0x110 : 0x111; // BTN_LEFT / BTN_RIGHT
}

private float toOutputX(float x) {
return inputRangeX > 0f && outputWidth > 0
? (x - inputMinX) * (outputWidth / inputRangeX) : x;
}

private float toOutputY(float y) {
return inputRangeY > 0f && outputHeight > 0
? (y - inputMinY) * (outputHeight / inputRangeY) : y;
}

/**
* Interpret one event from this device.
*
Expand Down Expand Up @@ -370,6 +413,12 @@ private boolean recognize(MotionEvent event) {
int action = event.getActionMasked();
int pointerCount = event.getPointerCount();

if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN
|| action == MotionEvent.ACTION_MOVE)
clickpadBaselineValid = true;
else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)
clickpadBaselineValid = false;

// Once declined, stay declined: the gesture is mid-way through being
// forwarded as touch and its pointer-up events still have to get there.
if (gestureUnhandled) {
Expand Down
Loading