├── .gitignore ├── settings.xml ├── Dockerfile-linuxbuild ├── src └── main │ └── java │ └── desktopKeyboard2Android │ ├── ShowJavaKeyEvents.java │ └── DesktopKeyboard2Android.java ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | target/ 4 | .classpath 5 | .project 6 | bin/ 7 | 8 | -------------------------------------------------------------------------------- /settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | com.zenjava 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dockerfile-linuxbuild: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | RUN apt update && apt install -y git wget sudo fakeroot 3 | RUN apt update && apt install -y java-common libgl1-mesa-glx libxxf86vm1 libcairo2 libgdk-pixbuf2.0-0 libpango1.0-0 libglib2.0-0 libatk1.0-0 libgtk2.0-0 libfontconfig1 libxtst6 libxrender1 libxi6 libasound2 maven 4 | RUN TEMP_DEB="$(mktemp)" && wget -O "$TEMP_DEB" 'https://cdn.azul.com/zulu/bin/zulu8.52.0.23-ca-fx-jdk8.0.282-linux_amd64.deb' && sudo dpkg -i "$TEMP_DEB" && rm -f "$TEMP_DEB" 5 | RUN git clone https://github.com/dportabella/DesktopKeyboard2Android.git 6 | WORKDIR DesktopKeyboard2Android 7 | COPY settings.xml . 8 | RUN mvn jfx:native -e -s"./settings.xml" 9 | -------------------------------------------------------------------------------- /src/main/java/desktopKeyboard2Android/ShowJavaKeyEvents.java: -------------------------------------------------------------------------------- 1 | package desktopKeyboard2Android; 2 | 3 | import javafx.application.Application; 4 | import javafx.scene.Scene; 5 | import javafx.scene.control.Label; 6 | import javafx.scene.input.KeyCode; 7 | import javafx.scene.input.KeyEvent; 8 | import javafx.stage.Stage; 9 | 10 | public class ShowJavaKeyEvents extends Application { 11 | final int LAST_KEY_CODE_AS_CONTROL_EVENT = KeyCode.DOWN.impl_getCode(); // 0x28 12 | final int LAST_CHAR_AS_CONTROL_EVENT = (int) ' '; // 0x20 13 | 14 | @Override 15 | public void start(Stage primaryStage) { 16 | primaryStage.setTitle("Keyboard"); 17 | Scene scene = new Scene(new Label(), 300, 275); 18 | primaryStage.setScene(scene); 19 | 20 | scene.setOnKeyPressed((KeyEvent e) -> { 21 | System.out.print("D[" + keyCodeToString(e.getCode()) + "] "); 22 | }); 23 | 24 | scene.setOnKeyReleased((KeyEvent e) -> { 25 | System.out.print("U[" + keyCodeToString(e.getCode()) + "] "); 26 | if (e.getCode().impl_getCode() == 27) { System.out.println(); } 27 | }); 28 | 29 | scene.setOnKeyTyped((KeyEvent e) -> { 30 | System.out.print("C[" + keyCharToString(e) + "] "); 31 | }); 32 | 33 | primaryStage.show(); 34 | } 35 | 36 | private String keyCodeToString(KeyCode keyCode) { 37 | return keyCode.getName() + ":" + keyCode.impl_getCode(); 38 | } 39 | 40 | private String keyCharToString(KeyEvent e) { 41 | String c = e.getCharacter(); 42 | String cStr = c.replaceAll("\r", ""); 43 | String codePointStr = (c.length() == 0) ? "(none)" : String.valueOf(c.codePointAt(0)); 44 | return cStr + ":" + codePointStr; 45 | } 46 | 47 | public static void main(String[] args) { launch(args); } 48 | } 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.githubdportabella 7 | DesktopKeyboard2Android 8 | 1.1-SNAPSHOT 9 | 10 | 11 | David Portabella 12 | 13 | 14 | 15 | https://github.com/dportabella/DesktopKeyboard2Android.git 16 | scm:git:git@github.com:tobrien/git-demo.git 17 | scm:git:git@github.com:dportabella/DesktopKeyboard2Android.git 18 | HEAD 19 | 20 | 21 | 22 | 1.8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.slf4j 29 | slf4j-api 30 | 1.7.7 31 | 32 | 33 | 34 | ch.qos.logback 35 | logback-classic 36 | 1.1.3 37 | 38 | 39 | 40 | ch.qos.logback 41 | logback-core 42 | 1.1.3 43 | 44 | 45 | 46 | 47 | 48 | 49 | com.zenjava 50 | javafx-maven-plugin 51 | 8.1.5 52 | 53 | desktopKeyboard2Android.DesktopKeyboard2Android 54 | 1.1 55 | true 56 | true 57 | DesktopKeyboard2Android 58 | 59 | 60 | 61 | org.apache.maven.plugins 62 | maven-release-plugin 63 | 2.5.3 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/desktopKeyboard2Android/DesktopKeyboard2Android.java: -------------------------------------------------------------------------------- 1 | /* 2 | This program, DesktopKeyboard2Android, captures the key events and forwards it to the Android WifiKeyboard app, through Wifi or USB. 3 | It gets the key events received from the laptop, and transform and forward them to WifiKeyboard app accordingly. 4 | This involves mainly, but not only, filtering out some of the KeyPressed/KeyReleased and KeyTyped events. 5 | Because of a lack of documentation about it, the logic implemented here has been based on experimenting, trail and error, 6 | and so it might not work in all systems and keyboards. 7 | See the README.md for my information. 8 | 9 | https://github.com/dportabella/DesktopKeyboard2Android 10 | */ 11 | 12 | package desktopKeyboard2Android; 13 | 14 | import javafx.application.Application; 15 | import javafx.scene.Scene; 16 | import javafx.scene.control.TextArea; 17 | import javafx.scene.input.KeyCode; 18 | import javafx.scene.input.KeyEvent; 19 | import javafx.stage.Stage; 20 | 21 | import java.io.BufferedReader; 22 | import java.io.IOException; 23 | import java.io.InputStreamReader; 24 | import java.net.HttpURLConnection; 25 | import java.net.URL; 26 | import java.nio.charset.StandardCharsets; 27 | import java.util.List; 28 | import java.util.stream.Collectors; 29 | 30 | public class DesktopKeyboard2Android extends Application { 31 | final int LAST_KEY_CODE_AS_CONTROL_EVENT = KeyCode.DOWN.impl_getCode(); // 0x28 32 | final int LAST_CHAR_AS_CONTROL_EVENT = (int) ' '; // 0x20 33 | 34 | final int ENTER_KEY_CODE = 10; 35 | final int ENTER_CODE_POINT = 13; 36 | final int ENTER_ANDROID_CODE_POINT = 10; 37 | 38 | private TextArea infoWindow; 39 | 40 | /* used to store and, if necessary, later forward a key used with modifiers (such as Crtl) 41 | * that has not been translated to a usefull typed key. 42 | * For instance: 43 | * Crtl-V produces a KeyTyped event that is not forwarded. So lastIgnoredKeyCode will be forwarded. 44 | * Alt-g in a Swiss-French keyboard produces a KeyTyped event (@ character) that is forwarded. So lastIgnoredKeyCode will not be forwarded. 45 | */ 46 | private KeyCode lastIgnoredKeyCode = null; 47 | 48 | private int keySequence = 0; 49 | 50 | @Override 51 | public void start(Stage primaryStage) { 52 | primaryStage.setTitle("Keyboard"); 53 | 54 | infoWindow = createInfoWindow(); 55 | infoWindow.setText(instrauctions); 56 | 57 | Scene scene = new Scene(infoWindow, 600, 400); 58 | primaryStage.setScene(scene); 59 | 60 | scene.setOnKeyPressed((KeyEvent e) -> { 61 | if (handleAsControlEvent(e.getCode())) { 62 | sendKeyPressed(e.getCode()); 63 | lastIgnoredKeyCode = null; 64 | } else { 65 | lastIgnoredKeyCode = e.getCode(); 66 | } 67 | }); 68 | 69 | scene.setOnKeyReleased((KeyEvent e) -> { 70 | if (handleAsControlEvent(e.getCode())) { 71 | sendKeyReleased(e.getCode()); 72 | } 73 | }); 74 | 75 | scene.setOnKeyTyped((KeyEvent e) -> { 76 | if (handleAsTypedEvent(e.getCharacter())) { 77 | sendKeyTyped(e.getCharacter()); 78 | } else if (lastIgnoredKeyCode != null) { 79 | sendKeyPressed(lastIgnoredKeyCode); 80 | sendKeyReleased(lastIgnoredKeyCode); 81 | lastIgnoredKeyCode = null; 82 | } 83 | }); 84 | 85 | primaryStage.show(); 86 | } 87 | 88 | private static TextArea createInfoWindow() { 89 | TextArea infoWindow = new TextArea(); 90 | infoWindow.setEditable(false); 91 | infoWindow.setDisable(true); 92 | return infoWindow; 93 | } 94 | 95 | private boolean handleAsControlEvent(KeyCode keyCode) { 96 | int code = keyCode.impl_getCode(); 97 | return (code != ENTER_KEY_CODE && code <= LAST_KEY_CODE_AS_CONTROL_EVENT); 98 | } 99 | 100 | private boolean handleAsTypedEvent(String c) { 101 | return ( (c.length() == 1 && c.charAt(0) == ENTER_CODE_POINT) // enter key 102 | || (c.length() > 0 && (c.length() > 1 || c.charAt(0) > LAST_CHAR_AS_CONTROL_EVENT))); 103 | } 104 | 105 | private void sendKeyPressed(KeyCode keyCode) { 106 | addInfo("D[" + keyCodeToString(keyCode) + "]"); 107 | sendKeyEvent("D" + keyCode.impl_getCode()); 108 | } 109 | 110 | private void sendKeyReleased(KeyCode keyCode) { 111 | addInfo("U[" + keyCodeToString(keyCode) + "]"); 112 | sendKeyEvent("U" + keyCode.impl_getCode()); 113 | } 114 | 115 | private void sendKeyTyped(String str) { 116 | addInfo("C[" + keyCharToString(str) + "]"); 117 | int c = str.codePointAt(0); // todo: is it possible to have more than one? 118 | int androidChar = (c == ENTER_CODE_POINT) ? ENTER_ANDROID_CODE_POINT : c; 119 | sendKeyEvent("C" + androidChar); 120 | } 121 | 122 | private String keyCodeToString(KeyCode keyCode) { 123 | return keyCode.getName() + ":" + keyCode.impl_getCode(); 124 | } 125 | 126 | private String keyCharToString(String c) { 127 | String cStr = c.replaceAll("\r", ""); 128 | String codePointStr = (c.length() == 0) ? "(none)" : String.valueOf(c.codePointAt(0)); 129 | return cStr + ":" + codePointStr; 130 | } 131 | 132 | private void sendKeyEvent(String keyEventString) { 133 | keySequence++; 134 | String url = "http://localhost:7777/key?" + keySequence + "," + keyEventString + ","; 135 | List lines; 136 | try { 137 | lines = httpRequest(url); 138 | } catch (IOException e) { 139 | addInfo("connection to androidWifiKeyboard app failed"); 140 | return; 141 | } 142 | if (lines.size() != 1 || !"ok".equals(lines.get(0))) { 143 | addInfo("unexpected response from android WifiKeyboard app: " + lines); 144 | } 145 | } 146 | 147 | private List httpRequest(String url) throws IOException { 148 | HttpURLConnection httpc = (HttpURLConnection) new URL(url).openConnection(); 149 | try (BufferedReader in = new BufferedReader(new InputStreamReader(httpc.getInputStream(), StandardCharsets.UTF_8))) { 150 | return in.lines().collect(Collectors.toList()); 151 | } 152 | } 153 | 154 | private void addInfo(String text) { 155 | infoWindow.appendText(text + "\n"); 156 | } 157 | 158 | public static void main(String[] args) { 159 | launch(args); 160 | } 161 | 162 | final static String instrauctions = 163 | "DesktopKeyboard2Android\n" + 164 | "Use your laptop's keyboard for typing in your Android phone\n" + 165 | "https://github.com/dportabella/DesktopKeyboard2Android/\n\n" + 166 | "- Install the WifiKeyboard app: https://play.google.com/store/apps/details?id=com.volosyukivan&hl=en\n" + 167 | "- Follow its instructions to connect your laptop and android together through wifi or usb.\n" + 168 | " this includes executing from the terminal: adb forward tcp:7777 tcp:7777\n" + 169 | "- Test that it works by browsing this web page from your laptop and type some text: http://localhost:7777/\n" + 170 | "- What you type here now, it will be forwarded to your Android\n\n"; 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DesktopKeyboard2Android 2 | ## Use your laptop's keyboard for typing in your Android phone 3 | 4 | [WifiKeyboard](https://play.google.com/store/apps/details?id=com.volosyukivan&hl=en) is a great android app that lets you use your laptop's keyboard to type into your android. You connect your laptop and android together by wifi or usb. In your laptop, you use a web browser, "the client", to type into your android. 5 | 6 | This project replaces "the client", while still using the android app. The differences are: 7 | * The client is a standalone software, instead of a web page 8 | * There are not parameters to set-up (so, no interaction with the application needed) 9 | * It works with non-US keyboards (eg, accents) while it also forwards combination keys. 10 | So for example, you can copy a text from an android app and paste it to another app by using Crt-C & Crtl-V (as if the keyboard was attached directly to the phone, without a laptop being present) 11 | 12 | 13 | ## Use 14 | - Install the [WifiKeyboard](https://play.google.com/store/apps/details?id=com.volosyukivan&hl=en) app 15 | - Follow its instructions to connect your laptop and android together through wifi or usb. 16 | This includes executing from the terminal: `adb forward tcp:7777 tcp:7777` 17 | - Test that it works by browsing this web page from your laptop and type some text: [http://localhost:7777/](http://localhost:7777/) 18 | - Install and run the [latest release of DesktopKeyboard2Android](https://github.com/dportabella/DesktopKeyboard2Android/releases) 19 | - What you type now in your laptop, it will be forwarded to your Android 20 | 21 | You may want to use a hot key (eg Alt+Cmd+X) to launch or re-activate both the `adb forward` command and DesktopKeyboard2Android. Follow the instructions for [OSX](http://www.cnet.com/news/how-to-use-hot-keys-to-launch-applications-in-os-x/), [MsWindows](https://www.google.com/search?hl=en&q=How+to+use+hot+keys+to+launch+applications+in+Ms+Windows) or [Linux](https://www.google.com/search?hl=en&q=How+to+use+hotkeys+to+launch+applications+in+Linux). 22 | 23 | 24 | ## Compile 25 | 26 | ### Retrieve source code 27 | 28 | $ git clone https://github.com/dportabella/DesktopKeyboard2Android.git 29 | $ cd DesktopKeyboard2Android 30 | 31 | ### Execute from source code 32 | 33 | To debug this application: 34 | 35 | $ mvn exec:java -Dexec.mainClass="desktopKeyboard2Android.DesktopKeyboard2Android" 36 | 37 | To show the java key events and understand how they are filtered and mapped for Android: 38 | 39 | $ mvn exec:java -Dexec.mainClass="desktopKeyboard2Android.ShowJavaKeyEvents" 40 | 41 | ### Build a native installer 42 | 43 | $ mvn jfx:native 44 | 45 | #### Java Multi-OS executable 46 | 47 | $ java -jar target/jfx/app/DesktopKeyboard2Android-1.1-SNAPSHOT-jfx.jar 48 | 49 | #### Linux Debian/Ubuntu package 50 | 51 | Install the Debian package: 52 | 53 | $ sudo dpkg -i target/jfx/native/desktopkeyboard2android-1.1.deb 54 | 55 | Start from command line: 56 | 57 | $ /opt/DesktopKeyboard2Android/DesktopKeyboard2Android 58 | 59 | Start from Unity launcher: search for "DesktopKeyboard2Android" (it's case insensitive). 60 | 61 | Start from other desktop: the application is installed under "All Applications" or "Unknown" category. 62 | 63 | There is a desktop starter available in `/opt/DesktopKeyboard2Android/DesktopKeyboard2Android.desktop`. 64 | If you copy it to `~/Desktop/` or `~/.local/share/applications/`, then you should edit it to add `Hidden=false`. 65 | You can also drag n' drop from nautilus to the Unity bar. 66 | 67 | #### Building using Docker 68 | 69 | To build using docker clone the project and then run the following commands: 70 | 71 | ``` 72 | sudo docker build . -tbuilddesktopkeyboard2android -f ./Dockerfile-linuxbuild 73 | sudo docker create --name builddesktopkeyboard2android builddesktopkeyboard2android:latest 74 | sudo docker cp builddesktopkeyboard2android:/DesktopKeyboard2Android/target /tmp/ 75 | sudo docker rm builddesktopkeyboard2android 76 | sudo docker rmi builddesktopkeyboard2android 77 | ``` 78 | 79 | #### Windows 80 | 81 | For creating the installer on MsWindows, you may need to download and install “Inno Setup 5” or later, see the output of the previous command. No requirements for OSX. 82 | 83 | 84 | ## Todo 85 | * Replace the GUI window by a system tray icon 86 | * Remove the need to first run the adb command 87 | * Put the key events in a queue, instead of blocking the application 88 | 89 | ## Notes for developers 90 | Java produces three types of events when typing text: `KeyPressed`, `KeyTyped` and `KeyReleased`. `KeyPressed` and `KeyRelease` have information about the key code, and it always corresponds to a single key. `KeyTyped` is generated by the operating system according to the keyboard layout configuration (eg, a US keyboard or Swiss-French keyboard). It may require to type several keys before getting a `KeyTyped` event. For instance, typing the 'Alt-\`' and then the 'e' keys on my Swiss-French keyboard generates a single KeyTyped event with the 'é' character. 91 | 92 | Some examples of Java KeyEvents: 93 | ``` 94 | D[] stands for KeyPressed, with keyCode name and number 95 | U[] stands for KeyReleased, with keyCode name and number 96 | C[] stands for KeyTyped, with character and codePoint 97 | 98 | a D[A:65] C[a:97] U[A:65] 99 | Shift D[Shift:16] U[Shift:16] 100 | Shift-a D[Shift:16] D[A:65] C[A:65] U[A:65] U[Shift:16] 101 | Space D[Space:32] C[ :32] U[Space:32] 102 | Enter D[Enter:10] C[:13] U[Enter:10] 103 | Esc D[Esc:27] C[:(none)] U[Esc:27] 104 | Tab D[Tab:9] C[:9] U[Tab:9] 105 | 106 | In My Swiss-French keyboard, I have a key that directly generates the accented letter 'è': 107 | è D[Open Bracket:91] C[è:232] U[Open Bracket:91] 108 | 109 | For generating 'é', I need to type to keys, first the 'Alt-`' and then the 'e': 110 | Alt` e D[Equals:61] C[:(none)] U[Equals:61] D[E:69] C[ê:234] U[E:69] 111 | 112 | For generating '@', I need to type Alt and G: 113 | alt-g D[Alt:18] D[G:71] C[@:64] U[G:71] U[Alt:18] 114 | 115 | For generating '#', I need to type Alt and 3: 116 | Alt-3 D[Alt:18] D[3:51] C[#:35] U[3:51] U[Alt:18] 117 | 118 | For generating '&', I need to type Shift and -6: 119 | Shift-6 D[Shift:16] D[6:54] C[&:38] U[6:54] U[Shift:16] 120 | 121 | The Up key does not fire a KeyTyped event, only the KeyPressed and KeyReleased event. 122 | Up D[Up:38] U[Up:38] 123 | 124 | Here I select three letters (with Shift Rights) and copy them to the clipboard (with Crtl-C): 125 | D[Shift:16] D[Right:39] U[Right:39] D[Right:39] U[Right:39] D[Right:39] U[Right:39] U[Shift:16] 126 | D[Ctrl:17] D[C:67] C[:3] U[C:67] U[Ctrl:17] 127 | ``` 128 | 129 | Note: sometimes, not always, key codes and code points have the same number. But they always have a different semantics. For instance: 130 | ``` 131 | KeyPressed/KeyReleased KeyTyped 132 | key code code point 133 | A 65 65 134 | a 65 97 135 | Up 38 - 136 | & - 38 137 | ``` 138 | 139 | The '&' character cannot typically be produced by pressing a single key (you need to type Shift-6 in my Swiss-French keyboard to generate it), and so it does not have a key code. 140 | Pressing the Up key does not produce a character, and so it does not have a code point. [Here](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/input/KeyCode.html) you have a list of key codes. 141 | 142 | Note: I do not understand why we get KeyTyped events with some key combinations that do not produce a character. For instance, typing Crlt-C generates also a KeyTyped event with code point 3. 143 | 144 | 145 | This software, DesktopKeyboard2Android, captures the key events and forwards them to the Android WifiKeyboard app, through Wifi or USB. 146 | 147 | Unfortunately, forwarding all key events to WifiKeyboard app generates duplicated or incorrect characters on the phone. For instance: 148 | - typing 'a' should generate only 'a', but WifiKeyboard will generate 'aa' (not even 'Aa', don't know why). 149 | - typing 'Alt-g' on my Swiss-French keyboard should generate only '@', but WifiKeyboard will generate 'G@'. 150 | 151 | One possibility is to forward only the KeyPressed and KeyReleased events. But then we miss the keyboard layout configuration. That is, typing first the 'Alt-\`' and then the 'e' in my Swiss-French keyboard, would generate two characters '\`e' instead of 'é' on the phone. That could be solved if WifiKeyboard app would allow selecting the layout configuration and generate itself the KeyTyped events accordingly. It would be like a combination of WifiKeyboard + [External Keyboard](https://play.google.com/store/apps/details?id=com.medion.android.keyboard&hl=en) apps. 152 | 153 | WikiKeyboard is open-source, but I could not work on it because of my poor working conditions (see below the Story of this application). So, my option was to get the key events received from the laptop, and transform and forward them to WifiKeyboard app accordingly. This involves mainly, but not only, filtering out some of the KeyPressed/KeyReleased and KeyTyped events. Because of a lack of documentation about it, the logic implemented here has been based on experimenting, trail and error, and so it might not work in all systems and keyboards. 154 | 155 | 156 | 157 | ## Story of this application 158 | I am trekking in Nepal Annapurna region for more than one month. I use my laptop to organize photos and type my journal. Suddenly, the retro-illumination of my screen stops working, which means that I can only see very small regions of my screen if I put a torch in front of it, very painful to work with. Typing my journal into my android phone is not feasible. The Google Keyboard app makes it a bit faster to type long texts, but it does not work in the Catalan language. 159 | 160 | I find the WifiKeyboard app, which allows me to use my laptop's keyboard to type into my android. That's fine for writing my journal. However, it is difficult to set-up each time that I want to type something, as the WifiKeyboard client requires to open a webpage in the laptop and click some parameters. That's difficult in my case as the laptop's screen is not working properly. There are two other issues: (1) I cannot type some of the accents on my Swiss-French keyboard, and (2) I cannot reorganize parts of the text as Crtl-C & Crtl-V do not work with the international keyboard. 161 | 162 | So I decided to develop this software to solve these issues while trekking in Nepal Annapurna, with elevations from 2000 to 5000m. I travel in a very relaxed way, trekking long and fast some days, and resting in a hut at the top of a mountain (Tilicho lake, Khopra, Moharedanda) for a few days. The working conditions are a bit painful, as my laptop screen is hardly working, the wifi and internet connection works slowly and intermittently, there are power cuts, I use IntelliJ in a very small screen (I mirror my laptop screen to my Samsung Galaxy S4 using Chrome Remote Desktop), and the whole set-up only works from time to time (anybody can send me detailed instructions on how to mirror my osx laptop to my android phone through usb?). It is also an opportunity to see how would it be to work at the mountains. It is great! No cars, not even bicycles, I can take a break, watch the great landscape, go for a walk, and maybe get a new picture of the problem I am trying to solve. Sometimes the set-up fails (power cut, no wifi/internet) and I am forced to take such a nice break. And no, this does not spoil my trekking vacations. I have just taken three days out of five weeks. It is a good small brake. :) 163 | 164 | Also, this is how it works in "remote areas". If I had been in a big city, I would have just taken my laptop to an Apple Center to fix it, or buy a new one, or buy an external USB keyboard for the android (which means you need money). In a remote area, people improvise solutions with whatever they have right there. Very nice! Also, it shows the big discrimination on access to information: in a big city, we can instantly browse a wikipedia page or watch a coursera.org video course. That was not the case here, with slow and intermittent internet connection. 165 | --------------------------------------------------------------------------------