Membuat Keyboard Jarak Jauh Untuk Android


Salah satu nilai tambah Input Method Framework (IMF) di Android adalah ia memungkinkan programmer untuk membuat Input Method Editor (IME) baru. Bagi pengguna awam, ini berarti membuat “aplikasi keyboard”, walaupun IME tidak harus selalu keyboard virtual. IME juga bisa dalam bentuk pengenalan suara, deteksi tulisan tangan, hasil baca dari kamera, dan berbagai kemungkinan lainnya.

Salah satu masalah yang saya hadapi berhubungan dengan input adalah sulitnya mengetik secara cepat di keyboard virtual (soft keyboard) Android. Ada beberapa solusi yang dapat ditempuh untuk mengatasi kendala tersebut. Misalnya, karena Android mendukung pemograman USB, saya bisa membuat sebuah hardware yang berfungsi sebagai keyboard fisik dengan mengikuti Android Open Accessory Protocol (AOA). Ini membutuhkan pengetahuan merakit hardware dan pemograman USB yang memadai. Selain itu, sebagai cara yang lebih murah, mengingat perangkat Android juga bisa berfungsi sebagai USB Host, maka keyboard dan mouse yang kompatibel bisa dihubungkan secara langsung melalui USB. Akan tetapi, karena hampir semua perangkat hanya datang dengan kabel micro USB yang berujung pada USB A male (untuk adaptor dan koneksi ke PC), saya perlu kabel USB yang berbeda. Untuk itu, saya bisa membeli sebuah kabel micro USB male yang berujung pada USB A female.

Alternatif lainnya, sebagai solusi yang murni melibatkan software saja, saya akan membuat sebuah IME yang menerima input melalui jaringan Wifi dari komputer. Tujuannya adalah agar saya bisa mengetik melalui PC yang terhubung ke jaringan lokal melalui Wifi. Karena ini hanya latihan, saya tidak akan memperhatikan aspek reliabilitas dan keamanan. IME akan membuka socket di port 9090. Siapa saja yang dapat menghubungi IP perangkat Android di port 9090 dapat langsung mengirim tulisan yang hendak diketik. Karakter ganti baris (\n) akan dipakai untuk menandakan bahwa input sudah selesai.

Saya akan mulai dengan membuat sebuah proyek baru di Android Studio. Karena kode program ini akan membuat socket, saya perlu menambahkan penggunaan permission berikut ini pada AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest ...>

  <uses-permission android:name="android.permission.INTERNET" />

  ...

</manifest>

Sebuah IME sebaiknya memiliki tampilan dimana pengguna dapat berinteraksi padanya. Pada soft keyboard, tampilan ini akan berupa deretan papan kunci yang dapat disentuh. Pada IME latihan ini, saya hanya akan menyediakan sebuah TextView yang berisi pesan informasi dan sebuah Button yang dapat dipakai untuk membatalkan input. Sebagai contoh, saya membuat layout bernama layout_input.xml yang isinya seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent"
    android:background="@android:color/black">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:id="@+id/message"
        android:textColor="@android:color/white"
        android:layout_alignParentEnd="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Batal"
        android:id="@+id/batal"
        android:layout_below="@+id/message"
        android:layout_centerHorizontal="true" />

</RelativeLayout>

Sebuah IME pada dasarnya sebuah service yang spesial dimana ia harus diturunkan dari InputMethodService. Sebagai contoh, berikut ini adalah implementasi lengkap dari service yang saya buat:

public class RemoteIME extends InputMethodService {

    private TextView pesanView;
    private String pesan;
    private ArrayBlockingQueue<String> pesanRemoteQueue = new ArrayBlockingQueue<>(100);
    private InputThread inputThread;

    @Override
    public void onCreate() {
        super.onCreate();
        ServerThread serverThread = new ServerThread();
        serverThread.start();
    }

    @Override
    public View onCreateInputView() {
        @SuppressLint("InflateParams") View view = getLayoutInflater().inflate(R.layout.layout_input, null);
        pesanView = (TextView) view.findViewById(R.id.message);
        pesanView.setText(pesan);
        view.findViewById(R.id.batal).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                hideWindow();
                if (inputThread != null) {
                    inputThread.selesai();
                }
                return true;
            }
        });
        return view;
    }

    @Override
    public void onStartInputView(EditorInfo info, boolean restarting) {
        inputThread = new InputThread(getCurrentInputConnection());
        inputThread.start();
    }

    private void tampilkanPesan(final String pesan) {
        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(new Runnable() {
            @Override
            public void run() {
                RemoteIME.this.pesan = pesan;
                if ((pesanView != null) && (pesan != null)) {
                    pesanView.setText(pesan);
                }
            }
        });
    }

    private class InputThread extends Thread {

        private InputConnection inputConnection;
        private boolean berjalan ;

        public InputThread(InputConnection inputConnection) {
            this.inputConnection = inputConnection;
        }

        @Override
        public void run() {
            berjalan = true;
            while(berjalan) {
                String pesan = pesanRemoteQueue.poll();
                if (pesan != null && inputConnection != null) {
                    getCurrentInputConnection().commitText(pesan, 1);
                    sendDefaultEditorAction(false);
                }
            }
            onFinishInput();
        }

        public void selesai() {
            berjalan = false;
        }

    }

    private class ServerThread extends Thread {

        @SuppressWarnings("InfiniteLoopStatement")
        @Override
        public void run() {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(9090);
                while (true) {
                    tampilkanPesan("Menunggu koneksi di port 9090");
                    try (Socket socket = serverSocket.accept()) {
                        tampilkanPesan("Terhubung dengan " + socket.getInetAddress().getHostAddress());
                        try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
                            String pesan;
                            while ((pesan = in.readLine()) != null) {
                                pesanRemoteQueue.offer(pesan);
                            }
                        }
                    }
                    tampilkanPesan("Koneksi ditutup");
                }
            } catch (final IOException e) {
                tampilkanPesan("Kesalahan koneksi: " + e.getMessage());
                Log.e("MyIME", "Kesalahan koneksi", e);
            } finally {
                if (serverSocket != null) {
                    try {
                        serverSocket.close();
                    } catch (IOException e) {
                        tampilkanPesan("Tidak dapat menutup socket: " + e.getMessage());
                        Log.e("MyIME", "Tidak dapat menutup socket", e);
                    }
                }
            }
        }

    }
}

Pada service di atas, terdapat dua buah nested class yang masing-masing mewakili thread terpisah, yaitu InputThread dan ServerThread. Saya perlu melakukan operasi yang melibatkan jaringan di thread terpisah agar aplikasi yang membutuhkan input tetap responsif.

Pada saat service pertama kali dijalankan, method onCreate() akan dipanggil. Method ini akan membuat thread ServerThread baru. Thread ini menggunakan ServerSocket untuk membuka socket yang menunggu koneksi di port 9090. Karena saya membuat looping tak terhingga dengan while (true), ia tidak akan pernah selesai menunggu tulisan yang dikirim dari klien yang terkoneksi padanya. Setiap kali ada pesan yang dibaca, thread ini akan meletakkannya pada sebuah queue. Saya perlu meletakkan pesan yang dibaca ke dalam queue karena pada saat klien menulis, belum tentu ada editor yang ingin membaca. Selain itu, karena kode program yang membaca nilai queue berada di thread berbeda, saya perlu menggunakan struktur data yang thread-safe seperti ArrayBlockingQueue.

Begitu ada operasi yang perlu menampilkan IME ini, method onCreateInputView() akan dipanggil bila belum pernah dipanggil sebelumnya. Pada saat editor siap untuk menerima masukan, method onStartInputView() akan dipanggil. Pada contoh di atas, method ini akan membuat thread InputThread baru. Thread ini akan terus membaca dari pesanRemoteQueue (dengan memanggil method poll()) selama belum dibatalkan oleh pengguna. Bila ada pesan yang berhasil dibaca dari pesanRemoteQueue, ia akan menuliskan pesan tersebut ke editor dengan memanggil getCurrentInputConnection().commitText(). Method getCurrentInputConnection() akan mengembalikan sebuah InputConnection yang merupakan penghubung utama antara IME dan editor (komponen visual yang memungkinkan input dari pengguna). Selain itu, saya juga memanggil method sendDefaultEditorAction() untuk mensimulasikan aksi default untuk editor tersebut (seperti halnya menekan tombol Enter dari soft keyboard).

Agar IME ini dikenali oleh sistem operasi Android di perangkat, saya perlu membuat sebuah pengenal dalam bentuk resource XML. Sebagai contoh, saya membuat file bernama method.xml di folder xml dengan isi seperti berikut ini:

<?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android">
    <subtype
        android:name="@string/label_subtype"
        android:imeSubtypeLocale="en_US"
        android:imeSubtypeMode="remote" />
</input-method>

Setelah itu, saya perlu mendaftarkan XML ini dengan mengubah isi AndroidManifest.xml menjadi seperti berikut ini:

<manifest ... >

  ...

  <application ...>
    <service ... android:permission="android.permission.BIND_INPUT_METHOD">
      <intent-filter>
        <action android:name="android.view.InputMethod" />
      </intent-filter>
      <meta-data android:name="android.view.im" android:resource="@xml/method" />
    </service>
  </application>

</manifest>

Setelah men-deploy aplikasi, saya kini bisa mengaktifkan IME baru ini dengan memilih Settings, Language and input. Saya bisa memberi centang pada nama aplikasi yang saya buat dan menjadikannya sebagai IME default.

Bila saya membuka sebuah aplikasi dan mencoba mengisi input dengan IME latihan ini, saya akan memperoleh tampilan seperti pada gambar berikut ini:

Tampilan IME

Tampilan IME

Untuk memulai mengetik dari PC, saya memastikan perangkat terhubung ke WiFi dan mencatat IP yang diberikan untuk perangkat Android tersebut. Kemudian, pada sebuah PC dengan sistem operasi Linux, saya memberikan perintah seperti berikut ini:

$ telnet x.x.x.x 9090
Trying x.x.x.x...
Connected to x.x.x.x.
Escape character is '^]'.
hi, lagi ngapain? aku sangat rindu padamu :)

Pada perangkat Android, apa yang saya ketik akan langsung muncul di aplikasi, seperti yang terlihat pada gambar berikut ini:

Hasil Pada Aplikasi Setelah Menerima Input

Hasil Pada Aplikasi Setelah Menerima Input

Akhirnya saya dapat mengetik dengan menggunakan keyboard fisik di perangkat Android tanpa mengeluarkan biaya sepeserpun untuk membeli kabel atau perangkat keras lainnya.

Perihal Solid Snake
I'm nothing...

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: