Skip to content
Open
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
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,63 @@ The main repo had stayed silent for 5 months, with nobody actually responding to
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio or gradle

## Troubleshooting

### Physical keyboard shortcuts (Alt+Tab, Win key, etc.) don't reach the host PC

Android intercepts certain keyboard shortcuts (Alt+Tab, Alt+Esc, Win, Ctrl+Esc, etc.) at the system level — they never reach the streaming session by default. Artemis ships with a small Accessibility service (`Artemis Physical keyboard`) whose only job is to forward those events to the host while you're streaming. You only need this if you stream with a physical keyboard.

Artemis will prompt you to enable this service the first time it detects a physical keyboard at app start. If you dismissed the prompt or want to enable it manually, there's also a button in **Settings → Input Settings → Forward system keyboard shortcuts**.

#### 1. Normal path

1. Open **Settings → Accessibility** on the device.
2. Find **Artemis Physical keyboard** in the list of installed services.
3. Toggle it on. Confirm the system warning about full device control.

#### 2. If the toggle won't stay on

On Android 13+, services in apps installed via sideload / ADB / Obtainium are blocked by **Restricted settings**. The toggle will appear to flip on, then quietly turn off again.

To unblock:

1. Open **Settings → Apps → Artemis** (the app's info page).
2. Tap the **⋮ (overflow menu)** in the top-right corner.
3. Choose **Allow restricted settings** (or **Restricted settings** → confirm).
4. Go back to **Accessibility** and toggle Artemis Physical keyboard on.

#### 3. If "Allow restricted settings" doesn't appear (some OEMs hide it)

Some custom Android skins (notably ZUI on Lenovo tablets) hide the **Allow restricted settings** entry entirely. Use ADB as a fallback:

1. Enable **Developer options → USB debugging** on the device.
2. Connect the device to a PC with `adb` installed and run:

```bash
# 1. Bypass the restricted-settings block for Artemis
adb shell appops set com.limelight.noir ACCESS_RESTRICTED_SETTINGS allow

# 2. Capture the current list of enabled accessibility services (do not lose this)
adb shell settings get secure enabled_accessibility_services

# 3. Append our service to that list (replace <existing> with the value from step 2;
# keep the colons separating each service)
adb shell settings put secure enabled_accessibility_services "<existing>:com.limelight.noir/com.limelight.KeyboardAccessibilityService"

# 4. Toggle accessibility off/on so the service actually binds
adb shell settings put secure accessibility_enabled 0
adb shell settings put secure accessibility_enabled 1

# 5. Verify the service is bound (look for "Artemis Physical keyboard" with capabilities=9)
adb shell dumpsys accessibility | grep "label="
```

If you're using the debug build, replace `com.limelight.noir` with `com.limelight.noirdebug`.

#### Why is this needed?

Android does not provide a public API for an app to capture system-level keyboard shortcuts at the activity level. The only sanctioned way to intercept events like Alt+Tab before SystemUI consumes them is via an Accessibility service with `FLAG_REQUEST_FILTER_KEY_EVENTS`. Artemis uses this purely to forward keys to the host while a stream is active — it does not read on-screen text, monitor your input outside of streaming, or collect any data.

## Authors

* [Cameron Gutman](https://github.com/cgutman)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ public void onServiceConnected() {
LimeLog.info("Keyboard service is connected");
AccessibilityServiceInfo info = new AccessibilityServiceInfo();
info.packageNames = new String[] { BuildConfig.APPLICATION_ID };
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK;
info.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
info.notificationTimeout = 100;
info.flags = AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN;
info.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;
setServiceInfo(info);
}

Expand Down
40 changes: 40 additions & 0 deletions app/src/main/java/com/limelight/PcView.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.HelpLauncher;
import com.limelight.utils.KeyboardAccessibilityHelper;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.UiHelper;
Expand Down Expand Up @@ -76,6 +77,7 @@ public class PcView extends AppCompatActivity implements AdapterFragmentCallback
private ShortcutHelper shortcutHelper;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
private boolean keyboardA11yPromptShown = false;
private ComputerDetails.AddressTuple pendingPairingAddress;
private String pendingPairingPin, pendingPairingPassphrase;
private final ServiceConnection serviceConnection = new ServiceConnection() {
Expand Down Expand Up @@ -378,6 +380,44 @@ protected void onResume() {

inForeground = true;
startComputerUpdates();

maybePromptForKeyboardAccessibility();
}

private void maybePromptForKeyboardAccessibility() {
if (keyboardA11yPromptShown || isFinishing() || isDestroyed()) {
return;
}
PreferenceConfiguration prefConfig = PreferenceConfiguration.readPreferences(this);
if (prefConfig.suppressKeyboardA11yPrompt) {
return;
}
if (!KeyboardAccessibilityHelper.hasPhysicalKeyboard(this)) {
return;
}
if (KeyboardAccessibilityHelper.isKeyboardServiceEnabled(this)) {
return;
}

keyboardA11yPromptShown = true;

AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(R.string.keyboard_a11y_prompt_title);
b.setMessage(R.string.keyboard_a11y_prompt_message);
b.setPositiveButton(R.string.keyboard_a11y_prompt_open_settings, (dialog, which) -> {
KeyboardAccessibilityHelper.openAccessibilitySettings(PcView.this);
});
b.setNeutralButton(R.string.keyboard_a11y_prompt_app_details, (dialog, which) -> {
KeyboardAccessibilityHelper.openAppDetails(PcView.this);
});
b.setNegativeButton(R.string.keyboard_a11y_prompt_dont_show_again, (dialog, which) -> {
PreferenceManager.getDefaultSharedPreferences(PcView.this)
.edit()
.putBoolean("checkbox_suppress_keyboard_a11y_prompt", true)
.apply();
});
b.setCancelable(true);
b.show();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public enum AnalogStickForScrolling {
private static final String CHECKBOX_FORCE_QWERTY = "checkbox_force_qwerty";
private static final String CHECKBOX_BACK_AS_META = "checkbox_back_as_meta";
private static final String CHECKBOX_IGNORE_SYNTH_EVENTS = "checkbox_ignore_synth_events";
private static final String CHECKBOX_SUPPRESS_KEYBOARD_A11Y_PROMPT = "checkbox_suppress_keyboard_a11y_prompt";
private static final String CHECKBOX_BACK_AS_GUIDE = "checkbox_back_as_guide";
private static final String CHECKBOX_SMART_CLIPBOARD_SYNC = "checkbox_smart_clipboard_sync";
private static final String CHECKBOX_SMART_CLIPBOARD_SYNC_TOAST = "checkbox_smart_clipboard_sync_toast";
Expand Down Expand Up @@ -194,6 +195,7 @@ public enum AnalogStickForScrolling {
private static final boolean DEFAULT_FORCE_QWERTY = true;
private static final boolean DEFAULT_SEND_META_ON_PHYSICAL_BACK = false;
private static final boolean DEFAULT_IGNORE_SYNTH_EVENTS = false;
private static final boolean DEFAULT_SUPPRESS_KEYBOARD_A11Y_PROMPT = false;
private static final boolean DEFAULT_ENABLE_FLOATING_BUTTON = false;
private static final boolean DEFAULT_BACK_AS_GUIDE = false;
private static final boolean DEFAULT_SMART_CLIPBOARD_SYNC = false;
Expand Down Expand Up @@ -255,6 +257,7 @@ public enum AnalogStickForScrolling {
public boolean forceQwerty;
public boolean backAsMeta;
public boolean ignoreSynthEvents;
public boolean suppressKeyboardA11yPrompt;
public boolean backAsGuide;
public boolean smartClipboardSync;
public boolean smartClipboardSyncToast;
Expand Down Expand Up @@ -1007,6 +1010,7 @@ else if (audioConfig.equals("51")) {
config.forceQwerty = prefs.getBoolean(CHECKBOX_FORCE_QWERTY, DEFAULT_FORCE_QWERTY);
config.backAsMeta = prefs.getBoolean(CHECKBOX_BACK_AS_META, DEFAULT_SEND_META_ON_PHYSICAL_BACK);
config.ignoreSynthEvents = prefs.getBoolean(CHECKBOX_IGNORE_SYNTH_EVENTS, DEFAULT_IGNORE_SYNTH_EVENTS);
config.suppressKeyboardA11yPrompt = prefs.getBoolean(CHECKBOX_SUPPRESS_KEYBOARD_A11Y_PROMPT, DEFAULT_SUPPRESS_KEYBOARD_A11Y_PROMPT);
config.backAsGuide = prefs.getBoolean(CHECKBOX_BACK_AS_GUIDE, DEFAULT_BACK_AS_GUIDE);
config.smartClipboardSync = prefs.getBoolean(CHECKBOX_SMART_CLIPBOARD_SYNC, DEFAULT_SMART_CLIPBOARD_SYNC);
config.smartClipboardSyncToast = prefs.getBoolean(CHECKBOX_SMART_CLIPBOARD_SYNC_TOAST, DEFAULT_SMART_CLIPBOARD_SYNC_TOAST);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,14 @@ public void initializePreferences() {
AppCompatActivity activity = (AppCompatActivity) requireActivity();
PackageManager pm = activity.getPackageManager();

Preference openA11y = findPreference("open_keyboard_a11y_settings");
if (openA11y != null) {
openA11y.setOnPreferenceClickListener(p -> {
com.limelight.utils.KeyboardAccessibilityHelper.openAccessibilitySettings(activity);
return true;
});
}

// hide on-screen controls category on non touch screen devices
if (!pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
PreferenceCategory category = findPreference("category_onscreen_controls");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.limelight.utils;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.Settings;
import android.view.InputDevice;
import android.view.accessibility.AccessibilityManager;

import com.limelight.BuildConfig;
import com.limelight.KeyboardAccessibilityService;

import java.util.List;

public final class KeyboardAccessibilityHelper {

private KeyboardAccessibilityHelper() {}

private static String getOurServiceId() {
return BuildConfig.APPLICATION_ID + "/" + KeyboardAccessibilityService.class.getName();
}

public static boolean isKeyboardServiceEnabled(Context ctx) {
AccessibilityManager am = (AccessibilityManager) ctx.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (am == null || !am.isEnabled()) {
return false;
}

String ourId = getOurServiceId();
List<android.accessibilityservice.AccessibilityServiceInfo> enabled =
am.getEnabledAccessibilityServiceList(android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
if (enabled == null) {
return false;
}
for (android.accessibilityservice.AccessibilityServiceInfo info : enabled) {
if (ourId.equalsIgnoreCase(info.getId())) {
return true;
}
}
return false;
}

public static boolean hasPhysicalKeyboard(Context ctx) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice device = InputDevice.getDevice(id);
if (device == null || device.isVirtual()) {
continue;
}
if ((device.getSources() & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD
&& device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
return true;
}
}
return false;
}

public static void openAccessibilitySettings(Activity activity) {
try {
Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
} catch (Exception ignored) {
}
}

public static void openAppDetails(Activity activity) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", activity.getPackageName(), null));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
} catch (Exception ignored) {
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/values-bg/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,14 @@
<!-- commit-text feature -->
<string name="title_enable_commit_text">Разреши изпращане на текст на блокове (жестово/гласово въвеждане)</string>
<string name="summary_enable_commit_text">Изпраща текст от софтуерната клавиатура наведнъж (жестово писане, предложения, гласово диктуване). Възможно е да не работи с игри, които разчитат на по-символно въвеждане.</string>

<string name="keyboard_a11y_prompt_title">Препращане на системните клавишни комбинации към компютъра?</string>
<string name="keyboard_a11y_prompt_message">Android прихваща клавишни комбинации като Alt+Tab, Alt+Esc и клавиша Win, преди да достигнат до хоста. За да ги препратите, активирайте \"Artemis Physical keyboard\" в Настройки за достъпност.\n\nАко превключвателят не остава включен, отворете страницата с информация за приложението и разрешете ограничените настройки, след което се върнете в Достъпност.</string>
<string name="keyboard_a11y_prompt_open_settings">Настройки за достъпност</string>
<string name="keyboard_a11y_prompt_app_details">Информация за приложението</string>
<string name="keyboard_a11y_prompt_dont_show_again">Не показвай повече</string>
<string name="title_keyboard_a11y_settings">Препращане на системните клавишни комбинации</string>
<string name="summary_keyboard_a11y_settings">Отворете Настройки за достъпност, за да активирате услугата за клавиатура Artemis. Необходимо за препращане на Alt+Tab, Alt+Esc, клавиша Win и други системни комбинации към компютъра хост.</string>
<string name="title_suppress_keyboard_a11y_prompt">Не питай за активиране на услугата за клавиатура</string>
<string name="summary_suppress_keyboard_a11y_prompt">Скрива подканата, която се показва при поточно предаване с физическа клавиатура, докато услугата за достъпност е изключена.</string>
</resources>
12 changes: 11 additions & 1 deletion app/src/main/res/values-ckb/strings.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="keyboard_a11y_prompt_title">قەدبڕەکانی سیستەم بۆ PC بنێردرێن؟</string>
<string name="keyboard_a11y_prompt_message">‏Android قەدبڕەکان وەک Alt+Tab، Alt+Esc و دوگمەی Win دەگرێتەوە پێش ئەوەی بگەن بە میوانخانە. بۆ ناردنیان، \"Artemis Physical keyboard\" چالاک بکە لە ڕێکخستنەکانی دەستڕاگەیشتن.\n\nئەگەر دوگمەکە چالاک نەمێنێتەوە، پەڕەی زانیاری ئەپەکە بکەرەوە و ڕێگە بدە بە ڕێکخستنە سنووردارەکان، پاشان بگەڕێرەوە بۆ دەستڕاگەیشتن.</string>
<string name="keyboard_a11y_prompt_open_settings">ڕێکخستنەکانی دەستڕاگەیشتن</string>
<string name="keyboard_a11y_prompt_app_details">زانیاری ئەپ</string>
<string name="keyboard_a11y_prompt_dont_show_again">دیسان نیشانی مەدە</string>
<string name="title_keyboard_a11y_settings">قەدبڕەکانی تەختەکلیلی سیستەم بنێرە</string>
<string name="summary_keyboard_a11y_settings">ڕێکخستنەکانی دەستڕاگەیشتن بکەرەوە بۆ چالاککردنی خزمەتگوزاری تەختەکلیلی Artemis. پێویستە بۆ ناردنی Alt+Tab، Alt+Esc، دوگمەی Win و قەدبڕەکانی تری سیستەم بۆ PC ـی میوانخانە.</string>
<string name="title_suppress_keyboard_a11y_prompt">داوای چالاککردنی خزمەتگوزاری تەختەکلیل مەکە</string>
<string name="summary_suppress_keyboard_a11y_prompt">ئەو پەیامە دەشاردرێتەوە کە دەردەکەوێت لە کاتی پەخشکردن بە تەختەکلیلی فیزیکی کاتێک خزمەتگوزاری دەستڕاگەیشتن ناچالاکە.</string>
</resources>
10 changes: 10 additions & 0 deletions app/src/main/res/values-cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,14 @@
<string name="pacing_latency">Preferovat nejnižší zpoždění</string>
<string name="title_enable_commit_text">Povolit vstup commit-textu (swipe/hlas)</string>
<string name="summary_enable_commit_text">Předává vícestránkový text z softwarových klávesnic (psaní gesty, návrhy, diktování). Může narušit hry, které vyžadují jednotlivé stisky kláves.</string>

<string name="keyboard_a11y_prompt_title">Přeposílat systémové klávesové zkratky do PC?</string>
<string name="keyboard_a11y_prompt_message">Android zachycuje klávesové zkratky jako Alt+Tab, Alt+Esc a klávesu Win dříve, než se dostanou k hostiteli. Pro jejich přeposílání povolte \"Artemis Physical keyboard\" v Nastavení přístupnosti.\n\nPokud přepínač nezůstane zapnutý, otevřete stránku informací o aplikaci a povolte omezená nastavení, poté se vraťte do Přístupnosti.</string>
<string name="keyboard_a11y_prompt_open_settings">Nastavení přístupnosti</string>
<string name="keyboard_a11y_prompt_app_details">Informace o aplikaci</string>
<string name="keyboard_a11y_prompt_dont_show_again">Příště nezobrazovat</string>
<string name="title_keyboard_a11y_settings">Přeposílat systémové klávesové zkratky</string>
<string name="summary_keyboard_a11y_settings">Otevřít Nastavení přístupnosti pro povolení služby klávesnice Artemis. Vyžadováno pro přeposílání Alt+Tab, Alt+Esc, klávesy Win a dalších systémových zkratek do hostitelského PC.</string>
<string name="title_suppress_keyboard_a11y_prompt">Nedotazovat se na povolení služby klávesnice</string>
<string name="summary_suppress_keyboard_a11y_prompt">Skrýt výzvu, která se zobrazí při streamování s fyzickou klávesnicí, když je služba přístupnosti vypnutá.</string>
</resources>
Loading